一、背景
针对项目已经开发完的接口,都需要加上传输数据加密的功能,对接口入参进行AES解密,对接口出参进行加密。考虑到尽量改动少点,使用自定义注解结合springmvc里的RequestBodyAdvice和ResponseBodyAdvice两个类进行实现。
RequestBodyAdvice允许针对接口请求体被读取之前进行修改,ResponseBodyAdvice允许接口出参在被返回之前进行修改。
二、实现
1、新建两个自定义注解类,用来标记哪些接口需要进行加密解密。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Encrypt {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Documented
public @interface Decrypt {
}
注意:@Decrypt配置的作用域是方法和参数上,@Encrypt则是只在方法上。
2、新建自定义DecryptRequestAdvice类继承RequestBodyAdviceAdapter,进行入参解密
@ControllerAdvice
@Slf4j
public class DecryptRequestAdvice extends RequestBodyAdviceAdapter {
@Autowired
private AesUtil aesUtil;
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// 当某个接口方法或参数上加了@Decrypt注解,才进行解密操作
return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
}
@Override
public HttpInputMessage beforeBodyRead(final HttpInputMessage inputMessage,
MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
InputStream inputStream = inputMessage.getBody();
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
String requestBody = new String(bytes);
try {
// 结合自己业务针对获取到的requestBody内容进行修改解密等操作
String decryptData = aesUtil.decrypt(requestBody , "www.cnblogs.com/shamo89");
// 把入参修改为解密后的内容
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decryptData.getBytes());
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
return byteArrayInputStream;
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
} catch (Exception e) {
log.error("接口入参解密出错:{}", Throwables.getStackTraceAsString(e));
}
return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
}
}
3、新建自定义EncryptResponseAdvice类继承ResponseBodyAdvice,进行出参加密
这里的R是自定义的接口返回封装类
@ControllerAdvice
@Slf4j
public class EncryptResponseAdvice implements ResponseBodyAdvice<R> {
@Autowired
private AesUtil aesUtil;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 只有接口上加了@Encrypt注解才进行出参加密操作
return returnType.hasMethodAnnotation(Encrypt.class);
}
@Override
public R beforeBodyWrite(R body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
try {
if (body.getData() != null) {
log.info("接口加密前返回的数据:{}", JSONUtil.toJsonStr(body.getData()));
String encStr = aesUtil.encrypt(JSONUtil.toJsonStr(body.getData()), "www.cnblogs.com/shamo89");
log.info("接口加密后返回的数据:{}", encStr);
body.setData(encStr);
}
} catch (Exception e) {
log.error("接口返回数据加密出错:{}", Throwables.getStackTraceAsString(e));
}
return body;
}
}
4、controller接口
@PostMapping("/test")
@ApiOperation(value = "测试接口加密解密")
@Encrypt
public R<UserInfoDTO> test(@Decrypt @RequestBody @Valid QueryVO vo) {
UserInfoDTO convert = Convert.convert(UserInfoDTO.class, vo);
return new R(convert);
}
三、原理说明
上面那两个advice类型,都要使用@ControllerAdvice注解进行修饰,它其实是一个实现特殊功能的@Component,只是针对controller进行拦截,本质还是aop,我们平常使用的全局异常处理类@ExceptionHandler,GlobalExceptionHandler,也是配合该注解使用。