spring mvc请求体偷梁换柱:HandlerMethodArgumentResolver

Published on 2021-10-09 15:40 in 分类: 博客 with 狂盗一枝梅
分类: 博客

最近有个需求要和外部对接,接口开放并且使用AES对称加密对请求体进行加密。流程上,我们系统会和对方系统进行数次交互,每次交互都要进行数据的加解密以及序列化和反序列化,如果不做统一处理的话,会很麻烦:

  1. 繁琐且冗余的操作很令人厌烦
  2. 数据交互都是加密后的字符串,在我们系统中使用了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注解之后会被其他内置的参数解析器拦截到

image-20211009151851091

序号为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); 
    } 
} 

#spring
目录