feat(wechat): 新增 WechatApiLoggingInterceptor 拦截器(含脱敏单元测试)
- ClientHttpRequestInterceptor 实现 - URL/method/status/content-type 用 INFO 级别 - body 用 DEBUG 级别,MAX_BODY_LOG=2048 截断 - 脱敏正则覆盖 session_key/openid/unionid/access_token/refresh_token/secret/appsecret - 配合 BufferingClientHttpRequestFactory 使用避免流被消费 - 4 个单元测试覆盖主要脱敏场景
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package com.emotion.interceptor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 微信 API HTTP 请求 / 响应日志拦截器
|
||||
*
|
||||
* <p>设计要点:
|
||||
* <ol>
|
||||
* <li>配合 BufferingClientHttpRequestFactory 使用,body 可被多次读取</li>
|
||||
* <li>URL / method / status / content-type 用 INFO 级别(生产可见)</li>
|
||||
* <li>body 用 DEBUG 级别(生产默认不输出,避免日志膨胀)</li>
|
||||
* <li>body 中敏感字段(session_key / openid / unionid / access_token / refresh_token)脱敏</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Slf4j
|
||||
public class WechatApiLoggingInterceptor implements ClientHttpRequestInterceptor {
|
||||
|
||||
private static final int MAX_BODY_LOG = 2048;
|
||||
private static final Pattern SENSITIVE_FIELDS = Pattern.compile(
|
||||
"\"(session_key|openid|unionid|access_token|refresh_token|secret|appsecret)\"\\s*:\\s*\"[^\"]*\"");
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(
|
||||
HttpRequest request, byte[] body,
|
||||
ClientHttpRequestExecution execution) throws IOException {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
log.info("[WeChatAPI] ---> {} {}", request.getMethod(), request.getURI());
|
||||
|
||||
ClientHttpResponse response = execution.execute(request, body);
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
|
||||
log.info("[WeChatAPI] <--- {} {} ({}ms) content-type={}",
|
||||
response.getStatusCode(), request.getURI(), duration,
|
||||
response.getHeaders().getContentType());
|
||||
|
||||
// body 读取(已由 BufferingClientHttpRequestFactory 缓存,可重复读)
|
||||
try {
|
||||
String bodyStr = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
|
||||
if (bodyStr.length() > MAX_BODY_LOG) {
|
||||
bodyStr = bodyStr.substring(0, MAX_BODY_LOG) + "...(truncated)";
|
||||
}
|
||||
log.debug("[WeChatAPI] body={}", desensitize(bodyStr));
|
||||
} catch (IOException e) {
|
||||
log.warn("[WeChatAPI] failed to read response body for logging", e);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏:将敏感字段值替换为 "***"
|
||||
*/
|
||||
private String desensitize(String body) {
|
||||
if (body == null || body.isEmpty()) {
|
||||
return body;
|
||||
}
|
||||
return SENSITIVE_FIELDS.matcher(body).replaceAll("\"$1\":\"***\"");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user