最近有个需求要和外部对接,接口开放并且使用AES对称加密对请求体进行加密。流程上,我们系统会和对方系统进行数次交互,每次交互都要进行数据的加解密以及序列化和反序列化,如果不做统一处理的话,会很麻烦:
- 繁琐且冗余的操作很令人厌烦
- 数据交互都是加密后的字符串,在我们系统中使用了swagger,swagger文档中显示的都是String类型的入参,接口文档就失去了作用
1.切面方法:行不通
基于以上两个问题,我首先想到了第一种解决方案:使用切面拦截Controller接口,然后解密并反序列化后反射执行controller中的方法
@Aspect
@Slf4j
@Component
public class HdxDecryptAspect {
@Around("@annotation(com.cosmoplat.qdind.config.web.annotation.HdxDecrypt)")
public Object pointCut(ProceedingJoinPoint point) throws Throwable {
log.info("进入解密切面");
MethodSignature signature = (MethodSignature) point.getSignature();
Class<?> targetClass = point.getTarget().getClass();
Method method = targetClass.getMethod(signature.getName(), signature.getParameterTypes());
HdxDecrypt hdxDecrypt = method.getAnnotation(HdxDecrypt.class);
if (hdxDecrypt == null) {
String classType = point.getTarget().getClass().getName();
Class<?> clazz = Class.forName(classType);
hdxDecrypt = clazz.getAnnotation(HdxDecrypt.class);
}
boolean decrypt = hdxDecrypt.decrypt();
Object[] args = point.getArgs();
//如果不需要解密,直接返回即可
if (!decrypt) {
return point.proceed(args);
}
List<Object> params = new ArrayList<>();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
if (args.length <= 0) {
return point.proceed(args);
}
Class<?>[] parameterTypes = method.getParameterTypes();
log.info("加密前的数据:{}", ObjectMapperFactory.getObjectMapper().writeValueAsString(args));
for (int i = 0; i < args.length; i++) {
Annotation[] parameterAnnotation = parameterAnnotations[i];
HdxDecrypt annotation = (HdxDecrypt) Arrays.stream(parameterAnnotation).filter(annotation1 -> annotation1 instanceof HdxDecrypt).findAny().orElse(null);
if (annotation.decrypt()) {
log.info("尝试解密数据{}",args[i].toString());
Object o = ObjectMapperFactory
.getObjectMapper()
.readValue(HdxAesUtil.decryptHex(args[i].toString()), parameterTypes[i]);
params.add(o);
continue;
}
params.add(args[i].toString());
}
log.info("解密后的数据:{}", ObjectMapperFactory.getObjectMapper().writeValueAsString(params));
return point.proceed(params.toArray());
}
}
在Controller层:
@PostMapping(value = "/syn")
@HdxDecrypt
public HdxResult<Boolean> syn(@HdxDecrypt ReqDTO reqDTO) {
try {
log.info(ObjectMapperFactory.getObjectMapper().writeValueAsString(reqDTO));
} catch (JsonProcessingException e) {
log.error("", e);
}
return null;
}
貌似没问题,实则行不通,尝试调用接口,jackson直接报错反序列化失败,这是因为jackson的反序列化动作优先级远高于切面的优先级。
2.自定义参数解析器:偷梁换柱
从目的上来看,想要的结果是外部请求传入加密的字符串,在Controller里直接接受反序列化好的Model,这里使用自定义的参数解析器可以解决该类问题。
第一步:实现HandlerMethodArgumentResolver接口
@Slf4j
public class HdxArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(HdxDecrypt.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HdxDecrypt parameterAnnotation = parameter.getParameterAnnotation(HdxDecrypt.class);
if (!parameterAnnotation.decrypt()) {
return mavContainer.getModel();
}
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
BufferedReader reader = servletRequest.getReader();
StringBuffer sb = new StringBuffer();
String str = null;
while ((str = reader.readLine()) != null) {
sb.append(str);
}
return ObjectMapperFactory
.getObjectMapper()
.readValue(HdxAesUtil.decryptHex(sb.toString()), parameter.getParameterType());
}
}
第二步:注册到参数解析器列表
@Configuration
@Slf4j
@AllArgsConstructor
public class WebMvcAutoConfiguration implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new HdxArgumentResolver());
}
}
第三步:修改Controller
这里,方法上删除自定义的注解,在请求体上添加自定义注解并且要删除RequestBody注解。
@PostMapping(value = "/syn")
public HdxResult<Boolean> syn(@HdxDecrypt ReqDTO reqDTO) {
}
3.自定义参数解析器遇到的问题
1.自定义参数解析器不生效
出现了一个怪事,无论如何自定义参数解析器都不生效,删除RequestBody注解就好了。
org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#getArgumentResolver
出现这个问题的原因是加上了RequestBody注解之后会被其他内置的参数解析器拦截到
序号为25的参数解析器是我自定义的参数解析器,序号为8的参数解析器是被选中的参数解析器,很明显,8号已经被选中了,所以不再往下匹配25号自定义的参数解析器,25号参数解析器就失效了。
2.在ServletRequest中取数据
在resolveArgument方法中貌似没有办法直接取出来请求体的数据,这里我直接使用了HttpServletRequest的方法读取了字符串数据,但是只能读取一次,如果想要多次读取,需要使用可重复读的流进行包装。详情可参考:http://cn.voidcc.com/question/p-ttriabfx-bko.html
@Component
public class CachingRequestBodyFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest currentRequest = (HttpServletRequest) servletRequest;
MultipleReadHttpRequest wrappedRequest = new MultipleReadHttpRequest(currentRequest);
chain.doFilter(wrappedRequest, servletResponse);
}
}