目录
- 一、日志能查错,也可能把不该保存的内容留下来
- 二、先确认日志到底保存哪些东西
- 三、用假响应检查请求与响应是否对称
- 四、参数数组不能靠字段名自动识别
- 五、响应也要经过结构化处理
- 六、解析失败时,不宜默认原样保存
- 七、先脱敏,再截断
- 八、业务隐私和凭据采用不同规则
- 九、验收脱敏要检查最终保存值
一、日志能查错,也可能把不该保存的内容留下来
接口对接需要请求、响应和任务记录,才能定位某张单失败在哪里。但日志脱敏不能只对请求对象里的password做一次替换。
我检查自己的日志模块时,发现它对请求调用了mask,对响应却直接序列化保存。这意味着“请求已经脱敏”不能证明整条日志都完成脱敏。
本文是本地代码审阅与构造样本复现。样本使用DEMO_ONLY等假值,没有读取账号文件,没有访问真实授权响应,也不能据此认定实际已经泄露凭据。文章由AI辅助整理,现有行为通过本地执行核对。
二、先确认日志到底保存哪些东西
项目日志记录包含连接标识、任务ID、动作、URL、请求、响应、HTTP状态、错误类别、耗时和时间。
这些字段服务于排错,但每个入口都需要自己的处理规则:
| 位置 | 需要关注的内容 |
|---|---|
| 请求对象 | 命名密钥字段、嵌套对象 |
| 参数数组 | 按位置传递的签名或密码 |
| 响应对象 | token、临时凭据、业务敏感数据 |
| URL | 查询参数中的敏感值 |
| 非JSON文本 | 错误页、文本错误信息 |
| 导出文件 | 导出前是否再次按用途过滤 |
通用递归函数主要解决命名字段。参数位置和响应结构,需要结合具体接口处理。
三、用假响应检查请求与响应是否对称
本地把日志模块的数据库写入替换为内存捕获,再给它一组假数据:
const{create}=require('./src/apilog');letsaved;constdb={prepare(){return{run(row){saved=row;}};}};constlog=create(db);log.record({connId:'test',action:'synthetic',url:'/example',request:{access_token:'DEMO_ONLY'},response:{access_token:'DEMO_ONLY'}});console.log(saved.request);console.log(saved.response);这段复现没有创建数据库,也没有发网络请求。使用相对require路径时,示例应放在该项目根目录下执行。
当前模块保存的结果分别是:
{"access_token":"***"}{"access_token":"DEMO_ONLY"}请求里的假token被遮住,响应里的假token仍保留。这是通用记录函数的行为,不是某个真实接口已经返回token的证据。
四、参数数组不能靠字段名自动识别
通用mask会遍历对象和数组,但是它依据对象字段名判断敏感内容。看到下面的对象时,它不知道第一个字符串表示什么:
{"parameters":["DEMO_SIGNATURE_ONLY"]}项目的金蝶客户端对此有单独处理:根据服务配置中的maskParams,把指定位置替换成星号,然后传给日志模块。
因此,不能仅凭通用mask无法识别位置参数,就推断当前签名登录链路没有遮蔽签名。应继续检查调用端是否预处理,以及服务配置的索引是否正确。
建议把位置规则绑定到具体接口动作,并针对该动作验证。不能给所有parameters数组使用同一套索引,因为各接口参数含义可能不同。
五、响应也要经过结构化处理
一个可行的改进方向,是让请求和响应走统一的日志净化入口,但使用各自适用的规则。
下面只展示“可解析JSON中的命名字段替换”,不是完整脱敏方案:
constsensitiveKeys=newSet(['password','pwd','appsecret','app_secret','access_token','refresh_token','token','authorization','cookie','set-cookie']);functionredactObject(value){if(Array.isArray(value))returnvalue.map(redactObject);if(value&&typeofvalue==='object'){returnObject.fromEntries(Object.entries(value).map(([key,item])=>[key,sensitiveKeys.has(key.toLowerCase())?'***':redactObject(item)]));}returnvalue;}如果响应是JSON字符串,先解析,再递归处理,最后序列化。仅把字符串作为普通值递归遍历,不会识别字符串内部的字段。
敏感字段清单要按实际协议补充。上面的示例不会自动识别所有命名变体,也不会发现普通字符串中夹带的凭据。
六、解析失败时,不宜默认原样保存
项目当前mask遇到不是JSON的字符串,会原样返回。这有利于保留错误信息,但不能保证文本不含敏感值。
建议对无法解析的内容采用明确策略:只保留状态、内容类型、长度及受控的诊断信息,或者使用针对该接口设计的文本过滤规则。需要原文进一步排查时,应放在受权限控制的诊断流程里。
URL也要单独处理。对请求体脱敏,不会顺便清除URL查询参数中的值。复制报文到工单或截图时,同样要检查地址、响应和异常文本。
七、先脱敏,再截断
超长报文需要限制大小,但截断不会替代脱敏。
如果先截断JSON,再尝试解析,报文可能已经不完整,解析失败后进入原文兜底,就失去结构化处理机会。较稳妥的顺序是:解析、净化、序列化、限制长度。
项目里长度限制的常量叫MAX_BYTES,实际使用的是字符串length和slice。它限制的是JavaScript字符串的UTF-16代码单元数量,不是UTF-8字节数。需要按磁盘或传输字节预算控制时,应明确按字节计算,并避免在多字节字符中间切断。
八、业务隐私和凭据采用不同规则
密钥一般不需要靠明文排错;客户名称、联系方式、金额和商品明细则可能对排查有用,但需要按用途和权限控制。
内部排错记录和对外分享截图应采用不同视图。分享时保留字段结构、错误阶段和必要状态,隐藏可识别客户的信息;不要认为token已经遮住,整张订单就可以直接公开。
日志保留时间也要明确。项目有按时间删除日志行的实现,但这不自动等于备份、导出文件及SQLite文件中的全部历史内容都被清除,相关存储需要分别安排。
九、验收脱敏要检查最终保存值
建议用完全虚构的标记构造测试:嵌套对象、对象数组、JSON字符串、非JSON文本、位置参数、URL参数和响应中的敏感字段。
验收看的是最终存储和导出结果,不能只检查mask函数是否存在。还要核对日志查看权限、异常文本及截图分享流程。
一套可用的日志应该能回答“哪张单、哪个阶段、什么错误”,同时按约定控制敏感内容。通用脱敏函数与接口专属规则一起检查,才知道哪些入口已经覆盖、哪些仍需补上。