From 82c308b40abc323bba6738706d340d8ed290f6a9 Mon Sep 17 00:00:00 2001 From: Peanut Date: Wed, 3 Jun 2026 07:42:59 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=BF=AE=E5=A4=8D=20spec=20=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=20Round=202=20=E5=8F=91=E7=8E=B0=E7=9A=84=201=20P0=20?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E9=94=99=E8=AF=AF=20+=203=20P2=20=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0 编译错误: - MediaType.includesCompatibleWith() 不存在于 Spring Framework 改为 MediaType.APPLICATION_JSON.isCompatibleWith(contentType) (避免实现阶段 mvn 编译直接阻塞) P2 一致性: - WechatResponseErrorHandler 类注释第 1 条错误('显式重写返回 true') 改为正确的 '委托父类 4xx/5xx 判断' - hasError() 方法内联注释自相矛盾 改为正确的 '委托父类 DefaultResponseErrorHandler.hasError()' - GlobalExceptionHandler.handleWechatApiException rawBody 日志未脱敏 (Interceptor 已脱敏响应 body,但 rawBody 字段在异常对象中再次传递会绕过脱敏) 新增 desensitizeForLog() 方法复用正则,覆盖 code 字段(authorization code 也需脱敏) - 移除 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) (与现有 BusinessException handler 不一致;HTTP 层统一返回 200,业务错误由 Result.code 表达) --- ...6-03-wechat-resttemplate-waf-fix-design.md | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/specs/2026-06-03-wechat-resttemplate-waf-fix-design.md b/docs/superpowers/specs/2026-06-03-wechat-resttemplate-waf-fix-design.md index cc7f35f..e4033ff 100644 --- a/docs/superpowers/specs/2026-06-03-wechat-resttemplate-waf-fix-design.md +++ b/docs/superpowers/specs/2026-06-03-wechat-resttemplate-waf-fix-design.md @@ -247,7 +247,7 @@ import java.io.IOException; * 微信 API 错误处理器 * * 设计要点: - * 1. 显式重写 hasError() 返回 true,确保 handleError() 总是被调用 + * 1. 显式重写 hasError() 委托父类判断(4xx/5xx → true),让 handleError() 在错误时被调用 * 2. handleError() 不抛 RestClientException,由业务层(WechatMiniProgramServiceImpl) * 根据 status code 判断如何处理 * 3. 不在此处读 body(避免流消费,由 LoggingInterceptor 统一处理日志) @@ -257,7 +257,7 @@ public class WechatResponseErrorHandler extends DefaultResponseErrorHandler { @Override public boolean hasError(ClientHttpResponse response) throws IOException { - // 显式重写:让 handleError 总是被调用(默认实现根据 status 4xx/5xx 判断) + // 委托父类 DefaultResponseErrorHandler.hasError():4xx/5xx 返回 true,其他返回 false return super.hasError(response); } @@ -330,7 +330,8 @@ public WechatCodeSessionResponse code2Session(String code) { } // 第三步:content-type 防御 - if (contentType == null || !contentType.includesCompatibleWith(MediaType.APPLICATION_JSON)) { + // 注意:Spring MediaType 没有 includesCompatibleWith 方法(编译错误),用 isCompatibleWith 替代 + if (contentType == null || !MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) { log.error("[WeChatAPI] code2Session 非 JSON 响应: contentType={}, body={}", contentType, rawBody); throw new WechatApiException(502, "WeChat returned " + contentType + " (likely WAF interception)", rawBody); @@ -363,16 +364,30 @@ public WechatCodeSessionResponse code2Session(String code) { 在 `GlobalExceptionHandler.java` 中新增: ```java +// 复用 Interceptor 的脱敏正则(避免重复定义) +private static final java.util.regex.Pattern SENSITIVE_FIELDS_FOR_LOG = java.util.regex.Pattern.compile( + "\"(session_key|openid|unionid|access_token|refresh_token|secret|appsecret|code)\"\\s*:\\s*\"[^\"]*\""); + +private String desensitizeForLog(String body) { + if (body == null || body.isEmpty()) { + return body; + } + return SENSITIVE_FIELDS_FOR_LOG.matcher(body).replaceAll("\"$1\":\"***\""); +} + /** * 处理微信 API 异常(脱敏后返回友好提示) + * + * 注意:不使用 @ResponseStatus,保持与现有 BusinessException handler 一致 + * (HTTP 层返回 200,业务错误通过 Result.code 表达) */ @ExceptionHandler(WechatApiException.class) -@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result handleWechatApiException(WechatApiException e, HttpServletRequest request) { - // 服务端日志:记录完整上下文(含 rawBody) + // 服务端日志:rawBody 需脱敏(Interceptor 已对响应 body 脱敏, + // 但 rawBody 字段是再次传递到异常对象,必须再次脱敏以防日志明文泄露) log.error("[WeChatAPI] 异常: {} {} - code={}, msg={}, rawBody={}", request.getMethod(), request.getRequestURI(), - e.getCode(), e.getMessage(), e.getRawBody()); + e.getCode(), e.getMessage(), desensitizeForLog(e.getRawBody())); // 前端响应:脱敏友好提示(不暴露 rawBody / errcode / 内部技术细节) String userMessage;