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); 
    } 
} 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章