最近有個需求要和外部對接,接口開放並且使用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);
}
}