接口日志与加密(SpringBoot)

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

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

一、记录请求/响应的参数

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中做。

二、请求解密/响应加密

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

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

但在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()*,否则将响应两次(原文一次,密文一次)。

三、使用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: 本文完整示例代码已上传至 Gitee

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值