介绍
在我们日常的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的底层过滤器不会过滤这些请求,最终两端应用都无法通信哦。