介绍

在我们日常的Java开发中,免不了和其他系统的业务交互,或者微服务之间的接口调用
如果我们想保证数据传输的安全,对接口出参加密,入参解密。

思路

  • 使用 @ControllerAdvice + RequestBodyAdviceAdapter 处理request进行解密;
  • 使用 @ControllerAdvice + ResponseBodyAdvice 对response进行加密;

实现

需要用到hutool工具类,帮助我们直接使用RSA加密

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.15</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <scope>compile</scope>
</dependency>

RSA加密解密工具类

public class RSAUtils {

    /**
     * 类型
     */
    public static final String ENCRYPT_TYPE = "RSA";

    /**
     * 从文件中读取公钥
     * @param filename 公钥保存路径
     * @return 公钥字符串
     * @throws Exception
     */
    public static String getPublicKey(String filename) throws Exception {
        //默认UTF-8编码,可以在构造中传入第二个参数做为编码
        FileReader fileReader = new FileReader(filename);
        String result = fileReader.readString();
        return result;
    }

    /**
     * 从文件中读取密钥
     * @param filename 私钥保存路径
     * @return 私钥字符串
     * @throws Exception
     */
    public static String getPrivateKey(String filename) throws Exception {
        //默认UTF-8编码,可以在构造中传入第二个参数做为编码
        FileReader fileReader = new FileReader(filename);
        String result = fileReader.readString();
        return result;
    }

    /**
     * 公钥加密
     * @param content   要加密的内容
     * @param publicKey 公钥
     */
    public static String encrypt(String content, PublicKey publicKey) {
        try {
            RSA rsa = new RSA(null, publicKey);
            return rsa.encryptBase64(content, KeyType.PublicKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 公钥加密
     * @param content   要加密的内容
     * @param publicKey 公钥
     */
    public static String encrypt(String content, String publicKey) {
        try {
            RSA rsa = new RSA(null, publicKey);
            return rsa.encryptBase64(content, KeyType.PublicKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 私钥解密
     * @param content    要解密的内容
     * @param privateKey 私钥
     */
    public static String decrypt(String content, PrivateKey privateKey) {
        try {
            RSA rsa = new RSA(privateKey, null);
            return rsa.decryptStr(content, KeyType.PrivateKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 私钥解密
     * @param content    要解密的内容
     * @param privateKey 私钥
     */
    public static String decrypt(String content, String privateKey) {
        try {
            RSA rsa = new RSA(privateKey, null);
            return rsa.decryptStr(content, KeyType.PrivateKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取公私钥-请获取一次后保存公私钥使用
     * @param publicKeyFilename 公钥生成的路径
     * @param privateKeyFilename 私钥生成的路径
     */
    public static void generateKeyPair(String publicKeyFilename, String privateKeyFilename) {
        try {
            KeyPair pair = SecureUtil.generateKeyPair(ENCRYPT_TYPE);
            PrivateKey privateKey = pair.getPrivate();
            PublicKey publicKey = pair.getPublic();
            // 获取 公钥和私钥 的 编码格式(通过该 编码格式 可以反过来 生成公钥和私钥对象)
            byte[] pubEncBytes = publicKey.getEncoded();
            byte[] priEncBytes = privateKey.getEncoded();

            // 把 公钥和私钥 的 编码格式 转换为 Base64文本 方便保存
            String pubEncBase64 = new BASE64Encoder().encode(pubEncBytes);
            String priEncBase64 = new BASE64Encoder().encode(priEncBytes);

            FileWriter pub = new FileWriter(publicKeyFilename);
            FileWriter pri = new FileWriter(privateKeyFilename);
            pub.write(pubEncBase64);
            pri.write(priEncBase64);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

准备三个注解

/**
 * 加密
 * @author killian
 * @since 2023/03/08
 */
@Documented
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decode {

}
/**
 * 加密
 * @author killian
 * @since 2023/03/08
 */
@Documented
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Encode {
}
/**
 * 组合注解,接受解密,返回加密
 * @author killian
 */
@Documented
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Decode
@Encode
public @interface Encrypt {
}

使用反射来判断controller,某个接口决定使用哪个注解,如果是Encrypt注解,那么加密和解密同时进行

/**
 * @author rstyro
 * @since 2020-10-20
 */
public class Utils {

    /**
     * 判断方法或类上有没有注解
     *
     * @param method      mothod对象
     * @param annotations 注解类数组
     * @param <A>         Annotation类型的class
     * @return boolean
     */
    public static <A extends Annotation> boolean hasMethodAnnotation(MethodParameter method, Class<A>[] annotations) {
        if (annotations != null) {
            for (Class<A> annotation : annotations) {
                if (method.hasMethodAnnotation(annotation) || method.getDeclaringClass().isAnnotationPresent(annotation)) {
                    return true;
                }
            }
        }
        return false;
    }
}

首先,准备一个读取yml配置类
它将用于是否开启统一加密和解密,以及是否打印解密数据等配置

@Configuration
public class KeyConfig {

    /**
     * rsa 公钥
     */
    @Value("${api.encrypt.rsa.publicKey}")
    private String rsaPublicKey;

    /**
     * rsa 私钥
     */
    @Value("${api.encrypt.rsa.privateKey}")
    private String rsaPrivateKey;

    /**
     * 是否打印解密数据
     */
    @Value("${api.encrypt.rsa.showLog}")
    private boolean showLog = false;

    /**
     * 是否开启加密和解密
     */
    @Value("${api.encrypt.rsa.open}")
    private boolean open = false;

    public String getRsaPrivateKey() {
        return rsaPrivateKey;
    }

    public String getRsaPublicKey() {
        return rsaPublicKey;
    }

    public boolean isShowLog() {
        return showLog;
    }

    public boolean isOpen() {
        return open;
    }
}

接着,配置publicKey和privateKey,大家可以通过RSAUtils的generateKeyPair方法生成

api:
  encrypt:
    rsa:
      publicKey: xxx
      privateKey: xxx
      showLog: true # 是否打印加密解密log true or false
      open: true # 是否开启加密 true or false

编写请求体加密

/**
 * @author killian
 * @since 2023/03/08
 */
@ControllerAdvice(basePackages = {"com.xxx.controller"})
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class EncryptRequestAdvice implements RequestBodyAdvice {

    @Autowired
    private KeyConfig keyConfig;

    /**
     * 是否需要解码
     */
    private boolean isDecode;

    @Override
    public boolean supports(@NotNull MethodParameter methodParameter, @NotNull Type type, @NotNull Class<? extends HttpMessageConverter<?>> aClass) {
        if (Utils.hasMethodAnnotation(methodParameter, new Class[]{Encrypt.class, Decode.class}) && keyConfig.isOpen()) {
            isDecode = true;
            // 这里返回true 才支持
            return true;
        }
        return false;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        //如果支持加密消息,进行消息解密。
        if (isDecode) {
            try {
                return new DecryptHttpInputMessage(inputMessage, keyConfig);
            } catch (Exception e) {
                log.error("Decryption failed", e);
            }
        }
        return inputMessage;
    }

    @Override
    public Object afterBodyRead(Object obj, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        // 这里就是已经读取到body了,obj就是
        return obj;
    }

    @Override
    public Object handleEmptyBody(Object obj, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        // body 为空的时候调用
        return obj;
    }
}
/**
 * 解码处理
 *
 * @author killian
 * @since 2023/03/08
 */
@Slf4j
public class DecryptHttpInputMessage implements HttpInputMessage {

    private HttpHeaders headers;

    private InputStream body;

    public DecryptHttpInputMessage(HttpInputMessage inputMessage, KeyConfig keyConfig) {
        // 这里是body 读取之前的处理
        this.headers = inputMessage.getHeaders();
        try {
            // 从inputStreamReader 得到aes 加密的内容
            String content = new BufferedReader(new InputStreamReader(inputMessage.getBody())).lines().collect(Collectors.joining(System.lineSeparator()));
            // 1、rsa解码
            String decryptBody = RSAUtils.decrypt(content, keyConfig.getRsaPrivateKey());
            if (keyConfig.isShowLog()) {
                log.info("Encrypted data received:{},After decryption:{}", content, decryptBody);
            }
            if (!StringUtils.isEmpty(decryptBody)) {
                // 4、重新写入到controller
                this.body = new ByteArrayInputStream(decryptBody.getBytes(StandardCharsets.UTF_8));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public InputStream getBody() {
        return body;
    }

    @Override
    public HttpHeaders getHeaders() {
        return headers;
    }
}

编写响应体解密

/**
 * 接口返回对象加密
 * @author killian
 */
@Slf4j
@ControllerAdvice(basePackages = {"com.xxx.controller"})
public class EncryptResponseAdvice implements ResponseBodyAdvice<Object> {

    @Autowired
    private KeyConfig keyConfig;

    private boolean encrypt;

    private static ThreadLocal<Boolean> encryptLocal = new ThreadLocal<>();

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        encrypt = false;
        if (Utils.hasMethodAnnotation(methodParameter, new Class[]{Encrypt.class, Encode.class}) && keyConfig.isOpen()) {
            encrypt = true;
        }
        return encrypt;
    }

    /**
     * 返回结果加密
     *
     * @param body               接口返回的对象
     * @param methodParameter    method
     * @param mediaType          mediaType
     * @param aClass             HttpMessageConverter class
     * @param serverHttpRequest  request
     * @param serverHttpResponse response
     * @return obj
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass,
                                  ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        Boolean status = encryptLocal.get();
        if (null != status && !status) {
            encryptLocal.remove();
            return body;
        }
        // 方法或类上有注解
        if (encrypt) {
            String publicKey = keyConfig.getRsaPublicKey();
            try {
                String content = JacksonUtil.writeJson(body);
                if (!StringUtils.hasText(publicKey)) {
                    throw new NullPointerException("Please configure rsa.encrypt.privatekeyc parameter!");
                }
                String result = RSAUtils.encrypt(content, publicKey);
                if (keyConfig.isShowLog()) {
                    log.info("Pre-encrypted data:{},After encryption:{}", content, result);
                }
                return result;
            } catch (Exception e) {
                log.error("Encrypted data exception", e);
            }
        }
        return body;
    }
}

注意

  • 该统一加密和解密,必须是@RequestBody注解,否则RequestBodyAdvice的底层过滤器不会过滤这些请求,最终两端应用都无法通信哦。
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐