接口日志与加密(SpringBoot)
在实际开发中,我们可能有如下需求:
- 记录请求/响应的参数,记录日志;
- 接口做加密防爬。即前后端约定好加密方式,前端传加密参数,后端获取到密文然后解密,处理完后再加密响应给前端。
1. 记录请求/响应的参数
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
中做。
2. 请求解密/响应加密
这个需求处理方式仍然是自定义HttpServletRequestWrapper
和HttpServletResponseWrapper
,因此,直接 copy 了ContentCachingRequestWrapper
和ContentCachingResponeWrapper
,并重写其中的几个方法。
2.1. 自定义HttpServletRequestWrapper
需要重写的方法
2.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.2. 自定义HttpServletResponseWrapper
需要重写的方法
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()
方法。
@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()*
,否则将响应两次(原文一次,密文一次)。
2.3. 使用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