跳至主要內容

接口日志与加密(SpringBoot)

Mayee...大约 3 分钟

在实际开发中,我们可能有如下需求:

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

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

Spring 已经提供好类可以使用:ContentCachingRequestWrapperContentCachingResponeWrapper。使用方式如下:

@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,实际上是,读取了请求体和响应体并缓存了起来,再构造了一个新的HttpServletRequestHttpServletResponse。而ContentCachingResponeWrapper并没有实现 flush 方法,响应给前端仍调用原 response 的方法,因此需要将ContentCachingResponeWrapper中的内容复制到原 response 中才可以响应给前端。 上述记录请求/响应内容,以及将响应内容复制给原 response ,也可以放在自定义的HandlerInterceptor中做。

2. 请求解密/响应加密

这个需求处理方式仍然是自定义HttpServletRequestWrapperHttpServletResponseWrapper,因此,直接 copy 了ContentCachingRequestWrapperContentCachingResponeWrapper,并重写其中的几个方法。

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();
            }
        }
    }

但在HandlerInterceptorafterCompletion方法中获取到的响应是加密后的,如果需要在此获取响应原文,则上述方法不重写,改为重写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();
    }
}

注意,在HandlerInterceptorafterCompletion方法中去掉*responseWrapper.copyBodyToResponse()*,否则将响应两次(原文一次,密文一次)。

2.3. 使用RequestBodyAdviceResponseBodyAdvice做参数记录或加解密

Spring 提共了RequestBodyAdviceResponseBodyAdvice接口,实现即可做参数记录或加解密操作。这种方式最为简单,但只能处理请求体参数,即@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: 本文完整示例代码已上传至 Giteeopen in new window