---
url: /article/9rsxx36x/index.md
---
在实际开发中，我们可能有如下需求：

1. 记录请求/响应的参数，记录日志；
2. 接口做加密防爬。即前后端约定好加密方式，前端传加密参数，后端获取到密文然后解密，处理完后再加密响应给前端。

## 1. 记录请求/响应的参数

Spring 已经提供好类可以使用：`ContentCachingRequestWrapper`和`ContentCachingResponeWrapper`。使用方式如下：

```java
@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. 解密请求体参数

```java
@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`需要重写的方法

```java
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()`方法。

```java
@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`处理。

```java
@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;
    }
}
```

```java
@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: 本文完整示例代码已上传至 [GitHub](https://github.com/Blogeee/blog-java/tree/main/_20211002)
