一,前言
在日常开发中,有时候经常需要和第三方接口打交道,有时候是我方调用别人的第三方接口,有时候是别人在调用我方的第三方接口,那么为了调用接口的安全性,一般都会对传输的数据进行加密操作,如果每个接口都由我们自己去手动加密和解密,那么工作量太大而且代码冗余。那么有没有简单的方法,借助 spring 提供的 @ControllerAdvice 注解搭配实现 RequestBodyAdvice 、 ResponseBodyAdvice 接口可以实现解密和加密操作。
二,@ControllerAdvice 原理
原理简述:SpringMVC 初始化的过程中,将会扫描所有带有 @ControllerAdvice注解的类,将其生成为 ControllerAdviceBean。如果这类刚好为 ResponseBodyAdvice接口的子类,Spring 将会为其单独保存起来,后续将会封装到的 RequestResponseBodyAdviceChain,使用责任链的模式对请求、响应进行处理。
实现 RequestBodyAdvice 、 ResponseBodyAdvice 接口同时需要搭配上 @ControllerAdvice 注解才能生效,为什么一定要搭配上 @ControllerAdvice 注解?首先我们看一下 @ControllerAdvice 的源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
可以看到这个注解上还存在一个我们非常熟悉的 @Component 注解。这里我们可以将 @ControllerAdvice 理解成 @Component 子类,所以其修饰的类也会成为 Spring 中 Bean。
ControllerAdviceBean
Spring 容器初始化过程,如果扫描到 @ControllerAdvice 注解,将会将其生成一个 ControllerAdviceBean 的Bean。这过程代码主要位于 RequestMappingHandlerAdapter#initControllerAdviceCache
:
private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
if (logger.isInfoEnabled()) {
logger.info("Looking for @ControllerAdvice: " + getApplicationContext());
}
// 获取所有被 @ControllerAdvice修饰的类。
List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(beans);
List<Object> requestResponseBodyAdviceBeans = new ArrayList<Object>();
for (ControllerAdviceBean bean : beans) {
Set<Method> attrMethods = MethodIntrospector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(bean, attrMethods);
if (logger.isInfoEnabled()) {
logger.info("Detected @ModelAttribute methods in " + bean);
}
}
Set<Method> binderMethods = MethodIntrospector.selectMethods(bean.getBeanType(), INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(bean, binderMethods);
if (logger.isInfoEnabled()) {
logger.info("Detected @InitBinder methods in " + bean);
}
}
//将所有实现了 RequestBodyAdvice 接口的 Bean 放入到 requestResponseBodyAdviceBeans 集合中,后续将会使用该集合。
if (RequestBodyAdvice.class.isAssignableFrom(bean.getBeanType())) {
requestResponseBodyAdviceBeans.add(bean);
if (logger.isInfoEnabled()) {
logger.info("Detected RequestBodyAdvice bean in " + bean);
}
}
//将所有实现了 ResponseBodyAdvice 接口的 Bean 放入到 requestResponseBodyAdviceBeans 集合中,后续将会使用该集合。
if (ResponseBodyAdvice.class.isAssignableFrom(bean.getBeanType())) {
requestResponseBodyAdviceBeans.add(bean);
if (logger.isInfoEnabled()) {
logger.info("Detected ResponseBodyAdvice bean in " + bean);
}
}
}
if (!requestResponseBodyAdviceBeans.isEmpty()) {
this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
}
}
第一步使用 ControllerAdviceBean#findAnnotatedBeans 获取所有被 @ControllerAdvice 修饰的类。这就是实现 RequestBodyAdvice 、 ResponseBodyAdvice 接口同时需要搭配上 @ControllerAdvice 注解拦截器才能生效的原因。
public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext applicationContext) {
List<ControllerAdviceBean> beans = new ArrayList<ControllerAdviceBean>();
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class)) {
if (applicationContext.findAnnotationOnBean(name, ControllerAdvice.class) != null) {
beans.add(new ControllerAdviceBean(name, applicationContext));
}
}
return beans;
}
第二步将所有实现了 RequestBodyAdvice 和 ResponseBodyAdvice 接口的 Bean 放入到 requestResponseBodyAdviceBeans 集合中,后续将会使用该集合。接下来我们来看下 ResponseBodyAdvice 的执行流程。
ResponseBodyAdvice
使用 IDEA 代码调试功能,然后查看代码调用栈:
如上面的所示,我们可以很清楚观察 ResponseBodyAdvice 调用关系。这里的类调用关系相对还是比较复杂,简单看还是 SpringMVC 的流程:
重点逻辑位于 RequestResponseBodyAdviceChain,我们具体看下源码:
@Nullable
private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request, ServerHttpResponse response) {
for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
if (advice.supports(returnType, converterType)) {
body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
contentType, converterType, request, response);
}
}
return body;
}
其实逻辑非常简单,遍历所有的 ResponseBodyAdvice 的子类,首先调用其 supports 判断是否支持,如果支持的调用的 beforeBodyWrite 处理返回信息。
三,Filter、Interceptor、ResponseBodyAdvice 区别
调用流程图:
Filter属于 Servlet 组件,所有请求将会先进入 Filter ,判断通过之后才会再进入到真正的具体的请求中。所有请求将会先进入到 Filter,通过之后才会进入到 SpringMVC 中最重要的组件 DispatchServlet。
而 Interceptor 是 SpringMVC 的组件,它的作用实际上与 Filter类似, 只不过的它的作用是位于自定义的 Controller 前后。
不管是 Filter 还是 Interceptor,它们的作用方法域内只能拿到 ServletResponse 的参数,这个时候返回值已经被写入 ServletResponse,没办法再去修改返回值。
而 ResponseBodyAdvice作用时机位于写入之前,所以这个时候可以很容易拿到原值进行修改。
四,代码实践
通过 RequestBodyAdvice 、 ResponseBodyAdvice 接口同时需要搭配上 @ControllerAdvice 注解实现具体的请求参数、返回结果的加解密逻辑,新增注解用于标注哪些接口需要加密或解密。
工具类准备:
加解密工具类 AesUtil:
@Slf4j
public class AesUtil {
private static final String keyStr = "123456";
private static final AES aes = SecureUtil.aes(keyStr.getBytes(StandardCharsets.UTF_8));
/**
* 加密
* @param content
* @return
*/
public static String encrypt(String content) {
String encryptHex = aes.encryptHex(content);
log.info("加密:原文:{},译文:{}", content, encryptHex);
return encryptHex;
}
/**
* 解密
* @param code
* @return
*/
public static String decode(String code) {
String decryptStr = aes.decryptStr(code, CharsetUtil.CHARSET_UTF_8);
log.info("解密:原文:{},译文:{}", code, decryptStr);
return decryptStr;
}
}
加密请求公共对象 EncRequestVO:
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel("EncRequestVO")
public class EncRequestVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "code")
private String code;
}
4.1,新增加解密注解
/**
* @title:解密
*/
@Target({METHOD, TYPE})
@Retention(RUNTIME)
@Documented
public @interface Decode {
}
/**
* @title:加密
*/
@Target({METHOD, TYPE})
@Retention(RUNTIME)
@Documented
public @interface Encrypt {
}
4.2,对返回结果加密处理 EncResponseAdvice
实现 ResponseBodyAdvice 接口,类上面添加 @ControllerAdvice 注解。
@Component
@ControllerAdvice
public class EncResponseAdvice implements ResponseBodyAdvice {
/**
* 选择哪些类,或哪些方法需要走beforeBodyWrite,从arg0中可以获取方法名和类名,arg0.getMethod().getDeclaringClass().getName()为获取方法名
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 判断返回 true,才会走 beforeBodyRead 方法逻辑。
return returnType.getMethodAnnotation(Encrypt.class) != null;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 加密
return AesUtil.encrypt(JSONUtil.toJsonStr(body));
}
}
4.3,对请求参数解密处理
需要解密的接口统一用 EncRequestVO 请求对象,将实际请求的参数通过加密后放入到 EncRequestVO 的字段中,由 EncRequestAdvice 进行解密成 Json 字符串,set 回 EncRequestVO 对象中,再传入到接口再做处理,接口端须将 Json 字符串转换成相对应的对象,再做处理。
@Component
@ControllerAdvice
public class EncRequestAdvice implements RequestBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// 判断返回 true,才会走 beforeBodyRead 方法逻辑。
return methodParameter.hasMethodAnnotation(Decode.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
String requestJsonStr = IOUtils.toString(inputMessage.getBody(), CharsetUtil.CHARSET_UTF_8);
System.out.println("=======requestJsonStr==========" + requestJsonStr);
AssertBizUtil.isTrue(StringUtils.isNotBlank(requestJsonStr), "传参为空异常");
// 规定:需要解密的接口统一用 EncRequestVO 请求对象。
EncRequestVO encRequestVO;
try {
encRequestVO = JSON.parseObject(requestJsonStr, EncRequestVO.class);
if (null != encRequestVO) {
String code = encRequestVO.getCode();
// 解密后将对象 set 回 EncRequestVO 中。
String jsonStr = AesUtil.decode(code);
encRequestVO.setCode(jsonStr);
return new ByteArrayInputStream(JSONUtil.toJsonStr(encRequestVO).getBytes(StandardCharsets.UTF_8));
}
return null;
} catch (Exception e){
throw new BBCCException(BBCCError.BUSINESS_ERROE,"传参解析异常");
}
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// 返回body
return body;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return null;
}
}
4.4,接口测试
代码:
@Decode
@Encrypt
@ApiOperation("留言列表-加密")
@PostMapping("/listV2")
public Result<PageResultVO<AllVO>> listV2(@RequestBody EncRequestVO encRequestVO){
String jsonStr = encRequestVO.getCode();
// 转换成实际的请求对象
MessageAO ao = JSON.parseObject(jsonStr, MessageAO.class);
PageResultVO<AllVO> pageList = service.list(ao);
return Result.success("查询成功", pageList);
}
@Decode:接口须对传参进行解密,传参必须为 EncRequestVO 对象,EncRequestAdvice 类中对将 EncRequestVO 里面的 code 字段进行解密。拿到解密后的 EncRequestVO,须转换成实际的请求对象,再做处理。
@Encrypt:添加此注解,经过 ResponseBodyAdvice 后,会将 Result 对象加密成字符串返回给前端。
至此,我们在尽可能少的修改controller的基础上,配合注解实现了全局接口的加密解密功能。
评论区