接口日志與加密(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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM