常见脱敏算法:替换、遮掩、混淆、泛化

脱敏算法这东西,说穿了就那几招。我最早入行时,以为脱敏就是简单的「把身份证号换成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)

混淆比替换复杂一些。它不是直接换成一个已知值,而是通过某种变换得到一个「看起来真实但实际上是假」的数据。

常见混淆方法:

  1. 哈希混淆:SHA-256 哈希后取前 N 位,保证长度固定
  2. 对称加密混淆:用密钥加密原始值,输出仍是可逆的
  3. 格式保持混淆(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岁分段
地理位置门牌号街道城市(不含邮编)

泛化还有一个亲戚叫「数据截断」。比如去掉邮箱的 @ 后面的域名,只保留用户名。原理相同。

泛化的核心优势:数据真实性最高。它不是造假,只是模糊了细节。所以很多数据分析场景,泛化是首选。

四种算法其实各有短板。替换破坏关联关系,遮掩容易残留信息,混淆需要密钥管理开销,泛化降低了数据精度。没有完美的方案,你得根据当前场景混合使用。

四种算法对比一览

算法数据真实性安全性可逆性典型场景
替换低中否测试库建假数据
遮掩中中否客服界面、日志脱敏
混淆高高可逆(有密钥)数据共享、外部系统对接
泛化极高低~中否数据统计、报表分析
常见脱敏算法体系 原始敏感数据 替换 遮掩 混淆 泛化 测试数据 展示数据 共享数据 统计报表 技术选型准则 精确标识查询 → 混淆|模糊统计分析 → 泛化 测试环境 → 替换|客服展示 → 遮掩