日志脱敏怎么做?从手机号掩码到结构化日志的完整实践

日志脱敏怎么做?从手机号掩码到结构化日志的完整实践 日志几乎是所有线上系统的基础设施。接口报错时需要查日志性能下降时需要分析调用链用户反馈问题时也经常需要根据用户 ID、请求时间和 Trace ID 定位具体请求。但日志记录得越详细泄露敏感数据的风险也越高。下面这些写法在业务代码里并不少见log.info(用户登录成功手机号{}密码{}, mobile, password); log.info(调用支付接口请求参数{}, request); log.error(接口请求失败Authorization{}, authorization); log.info(会议识别结果{}, transcript);这些日志在排查问题时确实方便但一旦日志平台权限配置不当、文件被下载或第三方组件发生漏洞敏感数据就可能直接暴露。因此日志设计不能只考虑“能否排查问题”还必须回答另一个问题在保留排障价值的同时如何尽量减少敏感信息进入日志这就是日志脱敏要解决的问题。一、哪些数据不应该原样写入日志并不是只有密码才属于敏感信息。在实际项目中下面这些数据都需要重点关注。数据类型常见示例建议处理方式密码password、newPassword禁止记录身份凭证Token、Cookie、Session ID删除或只保留少量前后缀手机号13812345678中间数字掩码邮箱userexample.com用户名部分掩码身份证号110101199001011234保留前后少量字符银行卡号6222021234567890仅保留后四位地址家庭地址、公司地址删除或模糊化生物信息人脸、声纹、指纹原则上禁止记录用户内容聊天记录、会议转写按业务需求最小化记录API 密钥appSecret、accessKey禁止记录支付信息卡号、验证码禁止记录需要注意的是敏感数据并不一定出现在明确命名的字段里。例如{ remark: 请把文件发到我的邮箱 userexample.com, content: 我的手机号是 13812345678 }remark和content本身看起来只是普通文本字段但内容里仍然可能包含敏感信息。所以日志脱敏通常需要同时处理两类数据结构化敏感字段 非结构化文本中的敏感内容二、日志脱敏的核心原则不是遮住所有内容日志脱敏不是简单地把所有字段替换成星号。如果日志最终变成这样用户 **** 请求 **** 失败参数 ****那么日志也失去了排障价值。更合理的目标是保留定位问题需要的信息 删除与排障无关的敏感内容例如手机号可以处理成138****5678邮箱可以处理成u***example.comToken 可以处理成eyJhbGci...8x2A这样既能区分不同用户或请求也不会完整暴露原始信息。可以将常见处理方式分为四类1. 删除适合密码、短信验证码、支付验证码等不应该进入日志的数据。password → 不记录 smsCode → 不记录 cvv → 不记录2. 掩码适合手机号、邮箱、身份证号等需要辅助定位的数据。13812345678 → 138****56783. 哈希适合需要进行一致性匹配但不需要查看原始值的数据。userexample.com ↓ b4c9a289323b21a01c3e940f150eb9b8相同的输入会生成相同结果因此仍然可以用于日志聚合和问题关联。4. 截断适合 Token、请求 ID 或较长文本。eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ↓ eyJhbGci...WCJ9三、不要直接打印整个请求对象下面这种代码非常常见log.info(创建用户请求{}, JSON.toJSONString(request));它的问题是请求对象中只要后来增加了敏感字段日志就会自动把新字段一起输出。例如最初的请求对象只有public class CreateUserRequest { private String nickname; private String email; }后来增加private String password; private String idCard;原来的日志代码没有发生变化但密码和身份证号已经被写进日志了。更安全的方式是主动选择需要记录的字段log.info( 创建用户请求nickname{}, email{}, request.getNickname(), maskEmail(request.getEmail()) );这种方式代码稍微多一些但日志内容更可控。对于复杂请求也可以创建专门的日志对象public record CreateUserLog( String nickname, String maskedEmail, String source ) { }记录日志时CreateUserLog logData new CreateUserLog( request.getNickname(), maskEmail(request.getEmail()), request.getSource() ); log.info(创建用户请求{}, logData);不要为了少写几行代码就把整个业务对象无差别序列化。四、常见脱敏函数怎么写可以先实现一组基础脱敏函数。1. 手机号脱敏public static String maskMobile(String mobile) { if (mobile null || mobile.length() 7) { return mobile; } return mobile.substring(0, 3) **** mobile.substring(mobile.length() - 4); }测试结果13812345678 → 138****56782. 邮箱脱敏public static String maskEmail(String email) { if (email null || !email.contains()) { return email; } int index email.indexOf(); String username email.substring(0, index); String domain email.substring(index); if (username.length() 1) { return * domain; } return username.substring(0, 1) *** domain; }测试结果developerexample.com → d***example.com3. 身份证号脱敏public static String maskIdCard(String idCard) { if (idCard null || idCard.length() 8) { return idCard; } return idCard.substring(0, 4) ********** idCard.substring(idCard.length() - 4); }4. Token 脱敏public static String maskToken(String token) { if (token null || token.length() 12) { return ***; } return token.substring(0, 6) ... token.substring(token.length() - 4); }这些函数并不复杂真正困难的是确保团队所有日志都按照同一规则处理。五、使用注解统一处理字段脱敏如果项目里需要脱敏的对象很多可以使用注解统一标记敏感字段。先定义脱敏类型public enum SensitiveType { MOBILE, EMAIL, ID_CARD, BANK_CARD, TOKEN }定义注解Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface Sensitive { SensitiveType type(); }业务对象中使用public class UserLogData { private Long userId; Sensitive(type SensitiveType.MOBILE) private String mobile; Sensitive(type SensitiveType.EMAIL) private String email; Sensitive(type SensitiveType.ID_CARD) private String idCard; }序列化日志时通过 Jackson 自定义序列化器读取注解并执行对应规则。这种方案的优点是脱敏规则集中管理 业务字段含义清晰 避免开发者每次手动调用函数 便于统一修改策略不过需要注意注解只有在指定的序列化流程里才会生效。如果开发者绕过日志序列化器直接调用字段的toString()仍然可能泄露原始值。六、通过 Logback 统一过滤日志是否可行有些项目会在 Logback 层增加正则过滤器对最终日志文本统一脱敏。例如检测手机号(?!\d)1[3-9]\d{9}(?!\d)替换为138****5678这种方式的优点是改动较小可以作为最后一道防线。但它也有明显局限正则可能误判 复杂文本识别不稳定 不同国家手机号格式差异很大 无法理解字段语义 会增加日志处理开销例如下面两个数字13812345678 13812345679可能是手机号也可能是业务编号。如果全部按手机号处理可能影响排障。因此更推荐的策略是业务代码主动控制字段 序列化层统一脱敏 日志框架正则过滤作为兜底不要把全部安全责任都交给一组正则表达式。七、结构化日志比字符串拼接更容易治理传统日志经常这样写log.info( 用户{}在{}登录失败手机号{}, userId, ip, mobile );随着字段增加日志很快会变得难以检索。结构化日志可以记录成{ event: USER_LOGIN_FAILED, userId: 10001, ip: 192.168.1.10, mobile: 138****5678, traceId: 8c04c66b }结构化日志的优势包括便于按字段检索 便于建立日志告警 便于集中执行脱敏策略 减少文案变化对查询的影响 便于接入 ELK、Loki 等系统尤其在微服务项目中推荐保留这些基础字段字段用途traceId串联一次完整调用spanId定位具体服务调用userId定位用户但避免记录更多身份信息event标识业务事件service标识服务名称environment区分测试和生产环境duration记录请求耗时result记录成功或失败errorCode记录稳定错误码有了这些字段很多问题不需要依赖完整请求内容也能定位。八、AI 应用中的日志脱敏更容易被忽略AI 应用经常需要处理大量非结构化内容例如用户提问 聊天记录 上传文档 语音转写结果 会议字幕 模型提示词 模型生成结果开发阶段为了排查模型效果开发者很容易直接记录logger.info(prompt%s, prompt) logger.info(response%s, response)这会带来两个问题。第一用户可能在提问或文档中输入手机号、邮箱、合同内容等隐私信息。第二提示词中可能包含企业知识库、内部规则或系统指令。因此AI 应用的日志策略通常应该更严格。建议至少区分开发环境日志 测试环境日志 生产环境日志 模型评估样本 用户授权的反馈数据生产环境中优先记录{ requestId: req_10001, model: model-a, inputTokens: 820, outputTokens: 246, latency: 1350, status: success }而不是默认记录完整 Prompt 和模型输出。九、实时语音和翻译系统应该记录什么实时语音识别、会议字幕和翻译系统也需要大量日志来排查延迟、断句和识别问题。例如同言翻译Transync AI这类具备实时翻译、双语字幕和会议总结功能的产品工程链路中可能涉及音频采集 语音活动检测 流式识别 翻译处理 字幕输出 会议总结为了定位问题可以记录{ meetingId: m_10001, segmentId: seg_102, sourceLanguage: en, targetLanguage: zh, asrLatency: 420, translationLatency: 180, status: success }一般不需要在普通业务日志中长期保存完整音频或完整字幕。如果确实需要采集错误样本也应该明确用途 限制访问权限 设置保留期限 取得必要授权 与普通日志分开存储问题排查所需的数据和长期保存用户内容并不是同一件事。十、日志平台的权限控制同样重要脱敏不是日志安全的全部。即使日志已经处理过也要控制访问范围。建议从以下几个方面治理1. 按环境隔离开发环境 测试环境 预发布环境 生产环境不同环境的日志不能混在一起。2. 按角色授权普通开发者不一定需要查看所有生产日志。可以区分只读权限 指定服务权限 敏感日志权限 导出权限 管理员权限3. 限制日志导出在线查询和批量下载的风险不同。批量导出通常应该有更严格的权限控制和审计记录。4. 设置保留周期日志不应该无限期保存。例如普通应用日志保留 30 天 安全审计日志保留 180 天 临时调试日志保留 3 天 错误样本按需求单独审批具体周期需要结合业务、法规和存储成本确定。十一、如何防止开发者误打敏感日志仅靠规范文档通常不够。更可靠的办法是把检查加入开发流程。1. 代码审查重点检查是否打印整个请求对象 是否打印 Authorization 是否记录密码和验证码 是否记录完整用户输入2. 静态扫描可以扫描常见危险代码log.*password log.*token log.*authorization log.*cookie System.out.println printStackTrace虽然不能覆盖所有问题但能拦截一部分明显风险。3. 自动化测试为脱敏函数编写单元测试Test void shouldMaskMobile() { assertEquals(138****5678, maskMobile(13812345678)); }还可以对日志输出进行测试确认敏感字段不会出现原文。4. 定期扫描日志平台主动搜索手机号格式 邮箱格式 身份证号格式 Authorization Bearer password secret发现泄露后不只是删除日志还要找到对应代码来源。十二、常见误区误区一生产环境不开 Debug 就安全了敏感信息可能出现在info和error日志中。关闭 Debug 不能替代脱敏。误区二日志只有开发人员能看日志可能被运维、测试、外包人员或第三方平台接触。还可能被下载、备份和长期存储。误区三数据已经加密所以可以打印数据库中的加密和日志中的明文是两回事。如果应用解密后再打印日志中仍然是原始内容。误区四用星号替换就完成了脱敏规则还要考虑可检索性、可定位性和不同业务的实际需求。误区五只处理明确字段用户输入、备注、聊天记录和模型 Prompt 等非结构化文本同样可能包含敏感信息。十三、日志脱敏检查清单项目上线前可以检查以下内容1. 密码、验证码、支付安全码是否禁止记录 2. Token、Cookie、Session ID 是否经过处理 3. 手机号、邮箱、身份证号是否脱敏 4. 是否存在直接打印整个请求对象的代码 5. 异常堆栈中是否可能包含请求参数 6. AI Prompt 和模型输出是否默认完整记录 7. 语音转写和会议字幕是否长期进入普通日志 8. 日志是否采用结构化字段 9. 是否保留 traceId 和稳定错误码 10. 日志平台是否按角色授权 11. 日志导出是否有权限和审计 12. 是否设置合理的日志保留周期 13. CI 中是否包含敏感日志扫描 14. 是否定期抽查生产日志总结日志脱敏的目标不是让日志失去信息而是在可观测性和数据安全之间找到平衡。一个相对完善的方案通常包括业务层只记录必要字段 敏感字段统一脱敏 结构化日志保留排障信息 日志框架增加兜底过滤 平台层实施权限与保留周期管理 开发流程加入自动检查日志真正应该回答的是哪个请求出了问题 问题发生在哪个环节 系统当时是什么状态而不是完整还原用户提交的所有内容。从这个角度看最好的日志不是记录得最多而是用最少的数据提供足够的排障依据。