我为什么把“日志脱敏”写进了代码规范的第一页
【摘要】 我为什么把“日志脱敏”写进了代码规范的第一页以前我打日志特别“豪放”。用户注册?把整个 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)