日志脱敏设计与实践
日志脱敏的核心是在安全与可排查之间取得平衡:既要避免敏感数据泄露,也要保留足够信息帮助定位问题。
后端日志是排查问题、追踪链路的核心依据,但也是敏感信息泄露的高风险入口。接口参数、返回值、第三方调用入参、异常上下文中,常包含手机号、邮箱、身份证号、银行卡号、Token 等。若完整写入日志并进入日志平台,会显著扩大敏感数据暴露面。
一、脱敏原则
1.1 显式敏感字段强制脱敏
字段名明确属于敏感信息时,直接脱敏,不依赖内容格式判断。
password, token, mobile, email, idCard, bankAccount这类字段一旦命中敏感字段集合,立即进入脱敏逻辑。
1.2 排查类 ID 字段默认保留
并非所有长数字都是敏感信息。大量业务排查依赖 ID:
id, userId, orderId, openId, unionId, traceId若全部脱敏,将严重影响问题定位。ID 类字段应放入排除规则,优先判断,不参与兜底扫描。
1.3 接口路径单独处理
请求路径用于定位接口,例如 GET /api/resource/detail。URI 不应按普通文本扫描,否则可能误伤路径参数,导致日志失去排查价值。
建议:路径中的敏感参数尽量避免放在 path 中,改为 body/query 参数并进入统一脱敏流程。
1.4 空值直接透传
null、空字符串不包含有效敏感信息,直接返回,避免无意义处理,也防止改变原始语义。
1.5 保留部分字符,兼顾排查体验
全部替换为 ****** 虽安全,但排查体验极差。推荐中间脱敏,保留前后部分信息:
| 类型 | 示例 |
|---|---|
| 手机号 | 138******1234 |
| 邮箱 | te******@example.com |
| 身份证 | 110101******1234 |
| 普通密钥 | ab******yz |
这样既降低泄露风险,又能判断是否传错账号、是否命中同一用户。
二、三层脱敏架构
2.1 第一层:结构化 JSON 脱敏(优先级最高)
接口请求参数、响应结果等结构化数据,优先按 JSON 解析后递归处理。
处理顺序:
1. 判断是否排除字段(ID 类)→ 原样保留
2. 判断是否敏感字段 → 按类型脱敏
3. 判断是否搜索参数上下文 → 高置信格式识别
4. 判断是否业务长文本且开启扫描 → 高置信格式识别
5. 递归处理对象或数组敏感字段优先级应高于文本扫描,排除字段也要尽早判断。
2.2 第二层:普通文本 Key-Value 脱敏(兜底)
非标准 JSON 日志,例如:
request params: {mobile=13812341234, password=abc123}通过 Key-Value 正则 进行兜底处理,匹配模式:
mobile=xxx
password: xxx
token='xxx'
email="xxx"只处理字段名明确的敏感内容,不扫描裸值,误判风险低。
2.3 第三层:高置信裸值扫描(按需开启)
适用于备注、描述、评论、内容等长文本字段。字段名本身不敏感,但文本中可能包含手机号、邮箱、身份证号。
必须通过配置显式开启,因为裸值扫描容易误伤业务编号、时间戳、流水号。
| 类型 | 是否建议默认扫描 | 原因 |
|---|---|---|
| 手机号 | ? 是 | 格式明确,误判低 |
| 邮箱 | ? 是 | 格式明确,误判低 |
| 身份证号 | ? 是 | 格式明确,误判低 |
| 银行卡号 | ? 否 | 长数字误判率高,除非业务场景明确 |
三、搜索参数的特殊处理
统一搜索参数常见结构:
{
"searchParam": {
"key": "13812341234"
}
}key、keyword、value 等字段名本身不敏感,容易漏脱敏。
引入"搜索上下文"机制:
进入 searchParam / searchParams 对象后
└─ 对子字段值进行高置信格式识别(手机号、邮箱、身份证)
└─ 仍然排除 userId、openId、unionId、xxxId 等字段覆盖搜索框输入敏感信息的场景,同时避免误脱敏普通业务 ID。
四、配置开关设计
至少提供两个独立开关:
log:
sensitive-mask-enable: true # 主开关:控制整体脱敏能力
sensitive-text-scan-enable: false # 子开关:控制业务长文本裸值扫描开关职责分离:
| 开关 | 控制范围 | 默认 |
|---|---|---|
sensitive-mask-enable | 所有脱敏逻辑 | true |
sensitive-text-scan-enable | 仅业务长文本裸值扫描 | false |
注意:显式敏感字段脱敏和 Key-Value 脱敏不依赖长文本扫描开关。若关闭裸值扫描后
password=xxx也不脱敏,属于设计缺陷。
五、ELK 链路中的脱敏接入点
在 ELK(Elasticsearch + Logstash + Kibana)或类似日志平台架构中,脱敏可以在三个环节介入,各有取舍:
| 接入点 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 应用层(推荐) | 日志框架 Filter/Encoder(如 Logback 的 MaskingMessageConverter、Log4j2 的 RewriteAppender) | 脱敏逻辑与业务代码同仓库,字段语义最准确,无网络传输损耗 | 每个应用都需要接入,升级需发版 | 核心服务、敏感字段明确的业务 |
| Filebeat/采集层 | Filebeat 的 processors 或自定义 Ingest Pipeline | 不侵入应用代码,统一管控 | 只能做正则/简单匹配,字段语义丢失,误判率高 | 存量系统无法改造时的兜底 |
| Logstash 处理层 | Logstash Filter(如 mutate + ruby 插件做脱敏) | 集中处理,便于统一规则变更 | 增加 Logstash 计算压力,延迟敏感场景需谨慎 | 多语言异构系统,统一脱敏策略 |
5.1 应用层脱敏(核心)
以 Logback 为例,实现 MaskingMessageConverter:
// 在 logback-spring.xml 中注册
<conversionRule conversionWord="maskedMsg"
converterClass="com.xxx.log.MaskingMessageConverter" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %maskedMsg%n</pattern>
</encoder>
</appender>为什么放在应用层:
- 能拿到字段名和类型信息,做结构化脱敏(JSON 递归、搜索参数上下文识别)
- 避免 Filebeat/Logstash 只能做纯文本正则,导致 ID 类字段被误伤、搜索参数漏脱敏
- 脱敏在序列化前完成,不依赖日志格式是否被换行、截断影响
5.2 Filebeat 兜底(辅助)
对于存量系统或第三方组件日志,Filebeat 可做轻量兜底:
# filebeat.yml
processors:
- script:
lang: javascript
source: >
function process(event) {
var msg = event.Get("message");
// 仅做高置信正则兜底,如手机号
msg = msg.replace(/\b1[3-9]\d{9}\b/g, "1**********");
event.Put("message", msg);
}注意:Filebeat 的脚本性能有限,不适合复杂逻辑,且无法区分
userId和mobile,容易误伤。
5.3 Logstash 统一处理(可选)
如果团队技术栈异构(Java/Go/Python 混用),可在 Logstash 做统一脱敏:
# logstash.conf
filter {
ruby {
code => "
msg = event.get('message')
# 简单 key-value 脱敏兜底
msg = msg.gsub(/(password|token|mobile)[=:]\s*[^\s,}]+/, '\\1=***')
event.set('message', msg)
"
}
}风险:Logstash 成为性能瓶颈,复杂正则可能导致日志堆积。建议仅做轻量兜底,核心脱敏仍下沉到应用层。
5.4 关键原则
| 原则 | 说明 |
|---|---|
| 字段语义脱敏 > 正则脱敏 | 应用层能识别 JSON 字段名、搜索参数上下文,ELK 链路后端只能做文本匹配 |
| 应用层优先,采集层兜底 | 核心敏感字段在应用层处理,存量/异构系统用 Filebeat/Logstash 做最低限度兜底 |
| 避免在 Kibana 侧做脱敏 | Kibana 只负责展示,数据已经落盘到 ES,脱敏无意义 |
| ES 中保留原始索引 vs 脱敏索引 | 如需保留原始日志用于审计,可双写:一个脱敏索引供日常排查,一个加密/受限访问的原始索引 |
六、常见误区与规避
| 误区 | 问题 | 正确做法 |
|---|---|---|
| 只做接口切面脱敏 | 无法覆盖业务代码手动打印的日志,如 log.info("calling service with params: {}", params) | 在日志编码器或上报链路增加全局脱敏能力,接口切面仅作辅助 |
| 只按字段名脱敏 | 漏掉搜索框、备注、描述等场景中的敏感内容 | 对特定上下文做补充扫描(搜索参数、长文本) |
| 对所有长数字脱敏 | 时间戳、主键 ID、订单号、流水号被误伤,日志可用性大幅下降 | 仅对高置信格式(手机号、邮箱、身份证)做裸值扫描 |
| 脱敏后重复处理 | 二次脱敏导致信息不可读 | 明确分支,命中规则后直接返回,避免叠加处理 |
| 忽略日志换行和编码 | 日志平台解析异常,甚至日志丢失或粘连 | 字节级处理时保留原始换行符、字符编码和日志格式 |
七、推荐处理流程
是否开启整体脱敏?
├─ 否 → 原样输出
└─ 是 → 继续
是否为结构化 JSON?
├─ 是 → 递归按字段处理
└─ 否 → 按普通文本处理
字段处理逻辑:
├─ 排除字段(ID 类)→ 原样保留
├─ 敏感字段 → 按字段类型脱敏
├─ 搜索参数上下文 → 高置信格式识别
├─ 业务长文本且开启扫描 → 高置信格式识别
└─ 普通文本 → 仅处理敏感 Key-Value设计优点
- 显式敏感字段不会漏
- 查询参数中的手机号/邮箱/身份证能覆盖
- 业务长文本通过开关可控
- ID 类字段不会被误伤
- 普通日志不会因为兜底扫描变得不可读
八、结论
日志脱敏不是简单的字符串替换,而是需要结合字段语义、日志形态、排查需求、误判风险的治理机制。
推荐策略:
| 场景 | 策略 |
|---|---|
| 字段名明确的敏感信息 | 强制脱敏 |
| 排查依赖的 ID 字段 | 明确排除 |
| 普通文本日志 | 仅处理敏感 Key-Value |
| 搜索参数 | 高置信格式识别 |
| 业务长文本 | 默认关闭,按需开启 |
| ELK 链路接入 | 应用层优先,采集层兜底,避免 Kibana 侧脱敏 |
| 架构层面 | 全局日志链路兜底,接口切面辅助 |
这样既能降低敏感信息进入日志平台的风险,也不会让日志失去排查价值。