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;