侧边栏壁纸
博主头像
再见理想博主等级

只争朝夕,不负韶华

  • 累计撰写 112 篇文章
  • 累计创建 64 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

基于RequestBodyAdvice与ResponseBodyAdvice实现统一加解密

再见理想
2022-09-19 / 0 评论 / 0 点赞 / 918 阅读 / 2,107 字

一,前言

在日常开发中,有时候经常需要和第三方接口打交道,有时候是我方调用别人的第三方接口,有时候是别人在调用我方的第三方接口,那么为了调用接口的安全性,一般都会对传输的数据进行加密操作,如果每个接口都由我们自己去手动加密和解密,那么工作量太大而且代码冗余。那么有没有简单的方法,借助 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;
	}

第二步将所有实现了 RequestBodyAdviceResponseBodyAdvice 接口的 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的基础上,配合注解实现了全局接口的加密解密功能。

0

评论区