在实际开发中,我们可能有如下需求:
记录请求/响应的参数,记录日志;
接口做加密防爬。即前后端约定好加密方式,前端传加密参数,后端获取到密文然后解密,处理完后再加密响应给前端。
一、记录请求/响应的参数 Spring 已经提供好类可以使用:ContentCachingRequestWrapper和ContentCachingResponeWrapper。使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component @WebFilter(filterName = "ContentCacheFilter", urlPatterns = "/**") public class ContentCacheFilter extends OncePerRequestFilter { @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper (request); ContentCachingResponeWrapper responseWrapper = new ContentCachingResponeWrapper (response); String requestBody = new String (requestWrapper.getContentAsByteArray()); filterChain.doFilter(requestWrapper, responseWrapper); String responseBody = new String (responseWrapper.getContentAsByteArray()); responseWrapper.copyBodyToResponse(); } }
请求经过 ContentCacheFilter 后,实际的 resquest 和 resposne 已经变成 requestWrapper 和 responseWrapper,实际上是,读取了请求体和响应体并缓存了起来,再构造了一个新的HttpServletRequest和HttpServletResponse。而ContentCachingResponeWrapper并没有实现 flush 方法,响应给前端仍调用原 response 的方法,因此需要将ContentCachingResponeWrapper中的内容复制到原 response 中才可以响应给前端。 上述记录请求/响应内容,以及将响应内容复制给原 response ,也可以放在自定义的HandlerInterceptor中做。
二、请求解密/响应加密 这个需求处理方式仍然是自定义HttpServletRequestWrapper和HttpServletResponseWrapper,因此,直接 copy 了ContentCachingRequestWrapper和ContentCachingResponeWrapper,并重写其中的几个方法。
1.自定义HttpServletRequestWrapper需要重写的方法 1.1 解密请求体参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public ServletInputStream getInputStream () throws IOException { if (this .inputStream == null ) { String body = readBody(getRequest()); UserModel userModel = new UserModel () .setId(1 ) .setUuid("YX8848" ) .setUname("解密后的用户" ); String decryptBody = JSON.toJSONString(userModel); this .inputStream = new ContentCachingInputStream (new ByteArrayInputStream (decryptBody.getBytes(StandardCharsets.UTF_8))); } return this .inputStream; }
这里重写了getInputStream(),解密了请求参数,并缓存起来。
2.自定义HttpServletResponseWrapper需要重写的方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 protected void copyBodyToResponse (boolean complete) throws IOException { if (this .content.size() > 0 ) { HttpServletResponse rawResponse = (HttpServletResponse) getResponse(); if ((complete || this .contentLength != null ) && !rawResponse.isCommitted()) { if (rawResponse.getHeader(HttpHeaders.TRANSFER_ENCODING) == null ) { rawResponse.setContentLength(complete ? this .content.size() : this .contentLength); } this .contentLength = null ; } encryptBody(); this .content.writeTo(rawResponse.getOutputStream()); this .content.reset(); if (complete) { super .flushBuffer(); } } }
但在HandlerInterceptor的afterCompletion方法中获取到的响应是加密后的,如果需要在此获取响应原文,则上述方法不重写,改为重写ServletOutputStream中的flush()方法。
1 2 3 4 5 6 7 8 9 10 11 @Override public void flush () throws IOException { if (!getResponse().isCommitted()) { JSONObject object = new JSONObject (); object.put("ciphertext" , "U2FsdGVkX18fFbYNhghNDR4o74uiS95ZbIs1dqGR50LVvmXavrreAInPfuRIZhVMT3mjzCcPeRa8=" ); byte [] bytes = object.toString().getBytes(StandardCharsets.UTF_8); ServletOutputStream outputStream = getResponse().getOutputStream(); outputStream.write(bytes); outputStream.flush(); } }
注意,在HandlerInterceptor的afterCompletion方法中去掉*responseWrapper.copyBodyToResponse()*,否则将响应两次(原文一次,密文一次)。
三、使用RequestBodyAdvice、ResponseBodyAdvice做参数记录或加解密 Spring 提共了RequestBodyAdvice和ResponseBodyAdvice接口,实现即可做参数记录或加解密操作。这种方式最为简单,但只能处理请求体参数,即@RequestBody修饰的参数,当工程中有全局异常处理,需要注意,若方法出现异常,会先进行全局异常处理,包装成正常响应,然后再经过ResponseBodyAdvice处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Slf4j @RestControllerAdvice(annotations = CryptoAdvice.class) public class CryptoRequestBodyAdvice implements RequestBodyAdvice { @Override public boolean supports (MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return true ; } @Override public HttpInputMessage beforeBodyRead (HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { return inputMessage; } @Override public Object afterBodyRead (Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { log.info("request encrypt body: {}" , body); JSONObject object = new JSONObject (); object.put("id" , 1 ); object.put("uuid" , "YX8848" ); object.put("uname" , "解密后的用户" ); return object.toJavaObject(targetType); } @Override public Object handleEmptyBody (Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { log.info("handleEmptyBody: {}" , body); return body; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Slf4j @RestControllerAdvice(annotations = CryptoAdvice.class) public class CryptoResponseBodyAdvice implements ResponseBodyAdvice <Object> { @Override public boolean supports (MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true ; } @Override public Object beforeBodyWrite (Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { log.info("response origin body: {}" , body); return new CipherText ("U2FsdGVkX18fFbYNhghNDR4o74uiS95ZbIs1dqGR50LVvmXavrreAInPfuRIZhVMT3mjzCcPeRa8" ); } }
Tip: 本文完整示例代码已上传至 Gitee