常见脱敏算法:替换、遮掩、混淆、泛化
脱敏算法这东西,说穿了就那几招。我最早入行时,以为脱敏就是简单的「把身份证号换成123456」,后来踩了不少坑才明白——选错算法,要么数据废了,要么没脱干净。今天咱们把四种最常用的算法掰开揉碎聊透。
替换(Replacement)
一种最直截了当的思路。找到敏感字段,用预设的替代值覆盖。你想想看,就像把机密文件直接换成一堆假文件,真文件锁柜子里。
替换又分两种:
- 静态替换:用固定值覆盖,比如把姓名全换成「张三」
- 动态替换:从字典池随机取替代值,每次结果可能不同
这里有个坑。我曾经在金融客户那遇到过一个情况——他们把所有客户手机号换成「13800138000」,结果一查询,几百万客户全是同一个号码。ETL 作业直接崩了,因为唯一索引冲突。这事之后,我养成了个习惯:替换前先检查目标列有没有唯一约束。
看看动态替换的伪代码实现:
def dynamic_replace(original, replacement_pool):
if original not in cache:
replacement = random.choice(replacement_pool)
cache[original] = replacement
return cache[original]
# 名字替换池
name_pool = ["张伟", "王芳", "李娜", "刘洋"]
# 每次调用,同一个输入得到同一个替换值
# 不同输入,分配的替换值不同
注意我用了 cache。为什么?为了保证映射一致性。你想想,同一个原始值如果每次替换结果不一样,业务分析里两张表 join 会出大问题。
遮掩(Masking)
说白了,遮掩就是让敏感信息「露一点但不全露」。最经典的例子:信用卡号只显示后四位,身份证号只显示前六位和后四位。
遮掩策略主要有三种:
| 策略 | 示例(原始:110101199001012345) | 适用场景 |
|---|---|---|
| 前置保留 | 110101*********2345 | 身份证号前端表示地域,后端校验位锁定身份 |
| 后置保留 | *********01012345 | 银行账号最后几位常用于对账 |
| 中间遮掩 | 110101****012345 | 通用做法,兼顾信息完整度 |
我曾经帮一家电商做订单日志脱敏。他们客服需要看到收件人姓名中的姓氏,以确认是本人,但又不允许看全名。最后方案:姓氏保留,名字用星号遮掩。「张**」这种格式正好满足需求。
遮掩的代码实现很简单:
def mask_middle(value, show_first=2, show_last=2, mask_char='*'):
if len(value) <= show_first + show_last:
return value
return value[:show_first] + mask_char * (len(value) - show_first - show_last) + value[-show_last:]
# 调用示例
mask_middle("13912345678", show_first=3, show_last=4)
# 输出:139****5678
混淆(Obfuscation)
混淆比替换复杂一些。它不是直接换成一个已知值,而是通过某种变换得到一个「看起来真实但实际上是假」的数据。
常见混淆方法:
- 哈希混淆:SHA-256 哈希后取前 N 位,保证长度固定
- 对称加密混淆:用密钥加密原始值,输出仍是可逆的
- 格式保持混淆(FPE):混淆后数据和原始数据格式完全一致,长度、字符集都不变
我记得有一次,一个医疗机构需要把病历号脱敏后供外部研究。他们想要哈希,但外部系统要求病历号必须是 12 位数字。SHA-256 输出是 64 位十六进制,直接截断会冲突。最后我选了 FPE,用 FF1 算法,输出正好 12 位数字,且保持唯一性。
FPE 的一个关键点——密钥管理。你的 HSM 里密钥生命周期管不好,混淆就形同虚设。这个我们在后面 HSM 章节会细聊。
一个简单的 FPE 示意(不涉及完整算法):
# FPE 的核心理念:输入一个 9 位数字
# 输出一个 9 位数字
# 通过 AES 加密 + 取模运算实现
def simple_fpe_encrypt(plaintext, key):
# 实际用 AES-FF1 或 AES-FF3
# 这里只展示输入输出格式
encrypted = aes_ff1_encrypt(plaintext, key, radix=10)
# encrypted 仍然是 9 位十进制数字
return encrypted
泛化(Generalization)
泛化是把精确值变模糊。年龄 28 变成「20-30 岁」,精确经纬度变成「北京市海淀区」。
泛化的层级决定了信息的精细度。你要是把「1992-05-15 14:30:00」泛化成「1992-05」,那分析精度降了太多。层级过低,脱敏等于没做;层级过高,数据废了。
我习惯用「数据可用性-安全性」的二维表来做决策:
| 维度 | 低安全需求 | 中安全需求 | 高安全需求 |
|---|---|---|---|
| 时间 | 精确到日 | 精确到月 | 精确到年 |
| 年龄 | 精确值 | 5岁分段 | 10岁分段 |
| 地理位置 | 门牌号 | 街道 | 城市(不含邮编) |
泛化还有一个亲戚叫「数据截断」。比如去掉邮箱的 @ 后面的域名,只保留用户名。原理相同。
四种算法其实各有短板。替换破坏关联关系,遮掩容易残留信息,混淆需要密钥管理开销,泛化降低了数据精度。没有完美的方案,你得根据当前场景混合使用。
四种算法对比一览
| 算法 | 数据真实性 | 安全性 | 可逆性 | 典型场景 |
|---|---|---|---|---|
| 替换 | 低 | 中 | 否 | 测试库建假数据 |
| 遮掩 | 中 | 中 | 否 | 客服界面、日志脱敏 |
| 混淆 | 高 | 高 | 可逆(有密钥) | 数据共享、外部系统对接 |
| 泛化 | 极高 | 低~中 | 否 | 数据统计、报表分析 |