在實際開發中,我們可能有如下需求:
- 記錄請求/響應的參數,記錄日志;
- 接口做加密防爬。即前后端約定好加密方式,前端傳加密參數,后端獲取到密文然后解密,處理完后再加密響應給前端。
一、記錄請求/響應的參數
Spring 已經提供好類可以使用:ContentCachingRequestWrapper
和ContentCachingResponeWrapper
。使用方式如下:
@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);
// request body
String requestBody = new String(requestWrapper.getContentAsByteArray());
filterChain.doFilter(requestWrapper, responseWrapper);
// response body
String responseBody = new String(responseWrapper.getContentAsByteArray());
// 將響應內容復制到原來的 response 中,前端才可以收到
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 解密請求體參數
@Override
public ServletInputStream getInputStream() throws IOException {
if (this.inputStream == null) {
// 解密后的請求體參數
// 讀取body參數, 解密操作 ...
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
需要重寫的方法
@Override
public void write(byte[] b, int off, int len) throws IOException {
originContent.write(b, off, len);
// 響應加密
String originBody = originContent.toString();
// do 加密
JSONObject object = new JSONObject();
object.put("ciphertext", "U2FsdGVkX18fFbYNhghNDR4o74uiS95ZbIs1dqGR50LVvmXavrreAInPfuRIZhVMT3mjzCcPeRa8=");
byte[] bytes = object.toString().getBytes(StandardCharsets.UTF_8);
content.write(bytes, 0, bytes.length);
}
但在HandlerInterceptor
的afterCompletion
方法中獲取到的響應是加密后的,如果需要在此獲取響應原文,則上述方法不重寫,改為重寫ServletOutputStream
中的flush()
方法。
@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
處理。
@Slf4j
@RestControllerAdvice(annotations = CryptoAdvice.class)//表示當類上有 CryptoAdvice 注解標記時,當前 RequestBodyAdvice 生效
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);
// do 解密
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;
}
}
@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