常见脱敏算法(一):替换、遮蔽、截断、偏移——原理与选型对比

脱敏算法选型这事儿,我踩过的坑比写过的代码还多。刚开始做数据安全时,总觉得“替换”就是万能的,后来被业务方怼回来三次才明白——没有最好的算法,只有最合适的场景。这一章咱们把四个基础算法掰开揉碎聊透。

1. 替换算法:最简单,但最容易被看穿

替换的本质是“一换一”。你把真实值扔进映射表,吐出来一个对应的假值。比如手机号脱敏,13800138000 变成 13912345678 —— 格式没变,长度没变,但内容完全变了。

我个人的习惯是:替换算法只用于不需要还原的场景,比如测试库。如果你需要数据还能被业务逻辑处理(比如保持手机号唯一性),那就必须用确定性替换——同样的输入永远输出同样的假值。

举个例子,我在一个电商项目中做过用户身份证替换。直接用随机数替换,结果数据分析组发现同一个用户的两张订单出现了不同身份证号,业务报表直接炸了。后来改成哈希映射 + 保留前6后4,才搞定。

// 确定性替换:基于哈希的映射
function deterministicMask(id: string, salt: string): string {
  const hash = crypto.createHash('sha256').update(id + salt).digest('hex');
  // 保留前6后4,中间用随机映射
  return id.substring(0,6) + hash.substring(0,4) + id.substring(id.length-4);
}
注意:替换算法最怕“反向推断”。如果映射表大小只有1000条,暴力枚举就能破。我曾经见过一个项目用线性同余生成器做映射,结果被安全审计一小时内全部破解——随机数种子和参数都被反推出来了。

2. 遮蔽算法:字段脱敏的标杆

遮蔽,说白了就是“盖住”。身份证中间8位打星,电话号码中间四位打星,银行卡号只留后四位。这是最符合直觉的脱敏方式——用户一看就懂,业务也能接受。

但是!遮蔽的坑在“遮蔽范围”。我记得有个银行项目,他们要对交易金额做遮蔽,规则是“大于100万的只显示万位”。结果业务员可以通过多次查询反推出精确金额——因为同一个账户每次遮蔽后剩下的部分是一样的,再结合其他维度就能还原。

这里我建议遵循一个原则:

  • 固定长度字段(身份证、手机号):遮蔽中间部分,保留前后少量标识
  • 变长字段(姓名、地址):遮蔽尾部或头部,保留能区分的最小长度
  • 数值字段:最好别用遮蔽,用偏移或区间替代
// 手机号遮蔽:保留前3后4
function maskPhone(phone: string): string {
  return phone.slice(0,3) + '****' + phone.slice(7);
}

3. 截断算法:简单粗暴,但丢失信息

截断就是“一刀切”。身份证只留前6位和后4位,中间全扔掉。这么做的好处是无法还原——信息都没了,更别说反推。但坏处也明显:数据可用性急剧下降。

我曾经在一个数据仓库项目里遇到这种情况:客户地址字段需要脱敏提供给第三方分析,我们用了截断,只保留省和市,区级以下全删。结果分析方拿到的数据完全没法做热力图——颗粒度太粗。最后只好改成“遮蔽区级,保留街道”,配合偏移做小区级的扰动。

截断的核心判断标准:丢失的信息是否会影响业务统计需求?如果只是做身份标识剥离,截断很合适;但如果是做分析,慎用。

4. 偏移算法:数值型字段的最佳拍档

偏移最适合处理金额、年龄、日期等数值。简单讲就是“晃一下”——真实值1000,偏移±10%,得到900~1100之间的随机数。这样单个值不准,但批量统计的均值、方差基本不变。

我记得有个征信评分项目,要求脱敏后仍然能按年龄段做分组统计。用遮蔽?不行,年龄被星号盖住后没法分组。用替换?也不行,因为替换会破坏年龄的分布。最后我们用偏移——每个人的年龄在±2岁范围内随机偏移,同时保证同一个人的年龄偏移量固定。这样统计结果和原始结果几乎一致。

偏移的关键参数有三个:

  1. 偏移幅度:±5%还是±20%?越大安全性越高,但统计失真越严重
  2. 偏移范围是否对称:对称偏移(±X)能保持均值不变,非对称偏移可能引入偏置
  3. 是否加噪声:高斯噪声 vs 均匀噪声,前者更接近真实分布
// 金额偏移:高斯噪声,标准差为金额的10%
function offsetAmount(amount: number): number {
  const noise = gaussianRandom() * amount * 0.1; // 高斯随机数
  return Math.round(amount + noise);
}

算法选型对比

说实话,没有银弹。我一般这样选:

算法 信息保留度 可逆性 适合数据类型 典型场景 避坑点
替换 高(格式一致) 可逆(有映射表) 标识符(手机号、证件号) 测试数据、跨系统同步 映射表泄露风险
遮蔽 中(保留部分) 不可逆 文本、长度固定字段 前端展示、日志脱敏 遮蔽范围过小可被反推
截断 低(丢失大量) 不可逆 需要剥离标识的数据 数据共享、匿名化 统计可用性差
偏移 中(统计保真) 不可逆 数值、日期、金额 数据分析、机器学习 偏移幅度影响精度
实战避坑:我曾经遇到一个需求——既要保留银行卡号的检验位(Luhn算法校验),又要脱敏。用替换?不行,替换后的卡号可能通过不了校验。用遮蔽?遮蔽中间的6位,前6后4保留,这样也能通过校验。最终方案:遮蔽中间6位,前6和后4保留,再重新计算一次Luhn校验位覆盖最后1位——既脱敏又合规。

选型决策流程

下面这张图概括了我平常的选型思路。你可以把它贴在工位旁边,每次接到脱敏需求先跑一遍这个流程。

脱敏算法选型决策流程 收到脱敏需求 需要保留原格式? 是 否 替换算法 数据类型:数值?文本? 数值 → 偏移算法 文本 → 遮蔽/截断 需要保留可读特征? 遮蔽算法 截断算法 提示:实际项目中常组合使用。例如身份证:前6位替换+中间8位遮蔽+后4位截断

嗯,这四种算法是脱敏的基石。它们单独用都有短板,但组合起来威力很大。你在做选型时,先别急着写代码——拿张纸像我上面那个流程图一样,把数据特性和业务需求列出来,每一步走下来,答案自然就出来了。