我为什么把“日志脱敏”写进了代码规范的第一页

举报
逆行时光 发表于 2026/11/11 09:31:47 2026/11/11
【摘要】 我为什么把“日志脱敏”写进了代码规范的第一页以前我打日志特别“豪放”。用户注册?把整个 User 对象 JSON.toString() 丢进去。下单?把请求参数原样打印。支付回调?把对方返回的完整报文记下来。我的逻辑很简单:日志越详细,排查问题越快。直到公司安全团队找上门,甩给我一份《日志审计风险报告》,里面标红了 200 多条记录——包含明文手机号、身份证号、银行卡号,甚至有几条完整的支付...

我为什么把“日志脱敏”写进了代码规范的第一页

以前我打日志特别“豪放”。用户注册?把整个 User 对象 JSON.toString() 丢进去。下单?把请求参数原样打印。支付回调?把对方返回的完整报文记下来。我的逻辑很简单:日志越详细,排查问题越快。直到公司安全团队找上门,甩给我一份《日志审计风险报告》,里面标红了 200 多条记录——包含明文手机号、身份证号、银行卡号,甚至有几条完整的支付密码。
那一刻我后背发凉。这些日志存在 ELK 里,所有有日志平台权限的人都能看到。万一有人导出数据、万一账号泄露、万一被拖库……这已经不是 Bug 的问题,是数据泄露事故,是要上新闻的。

一、日志里的“裸奔”数据,比你想的多

我认真扫了一遍项目里的日志代码,发现触目惊心:
// 场景一:打印完整请求对象
log.info("create order request: {}", JSON.toJSONString(createOrderReq));
// createOrderReq 里包含 userId、收货地址、手机号

// 场景二:打印异常时带上了上下文
log.error("payment failed, user: {}, cardNo: {}", userId, cardNo, e);
// cardNo 是明文银行卡号

// 场景三:打印第三方回调
log.info("wechat pay callback: {}", callbackXml);
// callbackXml 里包含用户 openid、金额、可能还有身份证信息
这些日志在排查问题时确实有用,但有用不代表可以裸奔。你永远不知道日志会被谁看到、被存多久、被怎么用。

二、脱敏不是“不打日志”,是“打该打的”

我重新设计了日志脱敏方案,核心原则:日志要能帮你排查问题,但不能帮别人窃取数据。
1. 定义敏感字段清单
我们先拉了一份清单,明确哪些字段必须脱敏:

类型
示例
脱敏规则
手机号
13812345678
前 3 后 4:138****5678
身份证
110101199001011234
前 6 后 4:110101********1234
银行卡
6222020200091234567
前 6 后 4:622202********4567
密码/Token
各种
全部替换为 ***
邮箱
zhangs**@**.com
前 2 后域名:zh***@qq.com
地址
详细地址
保留省市区,隐藏详细门牌
2. 统一脱敏工具类
写了个 SensitiveUtils,所有日志打印前过一遍:
public class SensitiveUtils {
    public static String maskPhone(String phone) {
        if (StringUtils.isBlank(phone) || phone.length() != 11) return phone;
        return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }
    
    public static String maskIdCard(String idCard) {
        if (StringUtils.isBlank(idCard) || idCard.length() < 8) return idCard;
        return idCard.replaceAll("(\\d{6})\\d+(\\d{4})", "$1********$2");
    }
    
    public static String maskBankCard(String cardNo) {
        if (StringUtils.isBlank(cardNo) || cardNo.length() < 10) return cardNo;
        return cardNo.replaceAll("(\\d{6})\\d+(\\d{4})", "$1********$2");
    }
}
3. 全局日志脱敏(进阶)
工具类靠人自觉,难免漏。后来我们加了一层全局拦截:自定义 Logback 的 MessageConverter,在日志输出前自动扫描消息体,匹配手机号、身份证号的正则模式,自动替换。
public class SensitiveMessageConverter extends MessageConverter {
    private static final Pattern PHONE_PATTERN = Pattern.compile("1[3-9]\\d{9}");
    
    @Override
    public String convert(ILoggingEvent event) {
        String msg = super.convert(event);
        if (msg != null) {
            msg = PHONE_PATTERN.matcher(msg).replaceAll("$1****$2");
            // 其他脱敏规则...
        }
        return msg;
    }
}
这样即使有人直接 log.info("user phone: {}", user.getPhone()),输出的也是脱敏后的。但全局正则有一定性能开销,我们只在生产环境开启。
4. JSON 序列化脱敏
对于 JSON.toJSONString() 的场景,用 Fastjson 的 ValueFilter 或 Jackson 的 JsonSerializer:
public class SensitiveJsonFilter implements ValueFilter {
    @Override
    public Object process(Object object, String name, Object value) {
        if (value == null) return null;
        if (name.contains("phone") || name.contains("mobile")) {
            return SensitiveUtils.maskPhone(value.toString());
        }
        if (name.contains("idCard") || name.contains("id_card")) {
            return SensitiveUtils.maskIdCard(value.toString());
        }
        // 更多规则...
        return value;
    }
}

三、一个容易忽略的坑:异常堆栈里的敏感信息

最隐蔽的风险在异常堆栈里。比如你校验参数时抛异常:
throw new IllegalArgumentException("invalid phone number: " + phone);
这个异常被全局异常处理器捕获,打日志时可能把完整堆栈输出。而 phone 是明文。我们后来统一:异常消息里不带敏感参数,敏感信息通过 log.error 单独打印(已脱敏)。

四、日志保留策略同样重要

脱敏是第一步,保留时间也要管。我们规定:
  • 业务日志:保留 30 天(排查问题够用了)
  • 安全审计日志:保留 1 年(合规要求)
  • 敏感操作日志(如登录、支付):保留 6 个月,且额外加密存储
超过保留期的日志自动清理,不累积。

五、一个心态的转变

以前我觉得日志脱敏是“安全团队的事”,现在觉得是开发的基本功。你写的代码产生的日志,里面有什么数据,你最清楚。安全团队不可能逐行审查你的日志代码,只有你自己能确保不把敏感信息打出来。
如果你现在的项目里还有 log.info("user: {}", JSON.toJSONString(user)) 这种代码,今晚就改。不用一步到位做全局脱敏,先从最明显的几个接口开始——用户注册、登录、支付、下单。每脱敏一个字段,就少一分数据泄露的风险。毕竟,日志平台不是保险箱,你打进去的每一行,都是潜在的暴露面。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。