• spring mvc请求体偷梁换柱:HandlerMethodArgumentResolver


    最近有个需求要和外部对接,接口开放并且使用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); 
        } 
    } 
    
  • 相关阅读:
    【泛型方法】
    【为什么使用泛型?】
    【泛型类】
    【泛型:ArrayListDemo】
    【泛型概述】
    【Collections:集合工具类:扑克游戏】
    【Collections:集合工具类:自然排序和比较器排序】
    Navicat for MYSQL 断网时本地连接无法打开,2005错误
    Navicat for MYSQL 数据库手动同步方法
    spring redis @Cacheable注解使用部分错误及无效原因
  • 原文地址:https://www.cnblogs.com/kuangdaoyizhimei/p/15386193.html
Copyright © 2020-2023  润新知