支付接口日志脱敏:别让 APIv3 密钥、账单下载签名和回调明文进日志
排查支付问题时最顺手的做法是把请求和响应都打出来。这些输出最终会落到日志系统里,而日志系统通常全员可读、留存 30~180 天,还会被同步到 ES / 云日志 / 工单截图。一次 debug 打开,敏感字段就扩散了。
下面按「哪几类字段最容易漏 → 具体从哪漏出去 → 怎么改」走一遍。大部分是工程判断,个别地方标注为未实测。识别方法本身可复现:拿你们的日志检索几条支付相关关键词,看命不命得中。
一、三类最容易漏的敏感字段
- 密钥与私钥。微信支付 APIv3 密钥(32 位)、商户私钥
apiclient_key.pem的内容、平台证书/公钥、Stripe 的sk_live_/rk_live_、支付宝的应用私钥字符串。这些本来只在初始化时读一次,但 SDK 抛异常时经常把整个配置对象序列化进堆栈。 - 回调/请求报文里的密文与明文。微信支付回调的
resource.ciphertext,以及你解密后的明文(含openid、金额、附言);请求体里的幂等键。密文本身还好,明文才是真正泄漏用户信息的那个。 - 带签名的临时 URL 与
Authorization头。最典型的是账单download_url——5 分钟有效、不做身份校验就能下载全量交易流水;还有Authorization: WECHATPAY2-SHA256-RSA2048 ...这种带签名的头,短时内可复用。
二、具体从哪漏出去
- 网关 access log:nginx 默认会记
$request_uri,账单下载的 query 里带着download_url的全套签名参数,一进日志就等于把「可下载全量流水」的链接抄送给了所有能看日志的人。 - HTTP 客户端 debug 开关:Go 里
httputil.DumpRequest/DumpResponse、Pythonrequests打开urllib3debug、PHP Guzzle'debug' => true,都会把Authorization和 body 原样打出来。这类开关上线后忘了关是常见事故。 - 异常处理:
catch之后 log 整个 request 对象;或者 SDK 异常 message 自带签名串(排查 401/SIGN_ERROR 时最容易顺手把签名粘贴进日志或工单)。 - 前端:JSAPI / 小程序的下单参数
paySign、signature被console.log到生产控制台,或被前端监控 SDK 一起上报。
三、改法:用白名单,不用黑名单
黑名单(列出「不要打哪些」)永远会漏。改成只打明确需要的字段。
微信支付排障真正需要的其实就几个:out_trade_no、transaction_id、trade_state、金额(分)、时间、错误码。其余一律不打。
Go + slog,用 ReplaceAttr 兜底,或者干脆只构造需要的字段:
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
switch a.Key {
case "authorization", "private_key", "api_v3_key", "ciphertext", "signature":
return slog.String(a.Key, "[REDACTED]")
}
return a
},
})
PHP Monolog 加一个 Processor,拦掉整段 $_POST/header:
$logger->pushProcessor(function ($record) {
foreach (['authorization', 'sign', 'private_key', 'ciphertext'] as $k) {
if (isset($record['context'][$k])) {
$record['context'][$k] = '[REDACTED]';
}
}
unset($record['context']['raw_body']); // 不要 log 整个请求体
return $record;
});
nginx 侧,账单下载这类接口走单独 location,并关掉或裁剪 query 记录:
log_format pay_clean '$remote_addr - $status $request_method $uri'; # 注意:用 $uri,不是 $request_uri(不含 query)
location /pay/bill/ {
access_log /var/log/nginx/pay_access.log pay_clean;
proxy_pass http://backend;
}
$uri 不含查询串,$request_uri 含。差异就在这一个变量上。
排障确实要看签名串时,别整段打:只留前后各 4 位(WECHATPAY2-…****),需要完整比对时用离线工具,不要进日志。
四、上线前花十分钟能做的检查
- 在日志系统里搜
Authorization、api_v3、ciphertext、sk_live、apiclient_key、download_url,看有没有命中。 - 确认所有 HTTP 客户端的 debug 开关在测试环境为真、生产环境为假(用配置项控制,不要靠人记得关)。
- 确认网关对账单/回执类 URL 不记完整 query。
- 确认异常上报(Sentry 之类)对 request body / headers 做了脱敏,很多 SDK 默认是「带上请求体」。
- 前端生产构建关掉相关
console.log,并检查监控 SDK 有没有把下单参数一起上报。
五、不适用 / 未实测
- 上面的
ReplaceAttr、Monolog Processor、nginxlog_format片段是常见写法,未在本机跑通具体版本,落地时按你项目的日志框架调整。 - 具体 SDK 是否默认打请求体、异常上报平台默认带哪些字段,各版本不同,需自行验证。
- 「日志留存 30~180 天、全员可读」是常见配置,不是规定;真正要紧的是先确认你们自己的日志谁能看、留多久——这决定了泄漏面有多大。