80 个敏感项全部正确脱出(身份证、手机号、邮箱、银行卡各 20 个)。 但另外造的三条 19 位订单号连 Luhn 校验都能过,被一起打码——误伤 3 个。 别名表 83 个,一致性 83/83:同一原值只对应一个假名。
上一节讲多模态。这一节讲脱敏——它和多模态不冲突,是同一份数据在出口处的两道处理。
一、脱敏不等于匿名
这两个词经常混用,但后果完全不同。脱敏是「把敏感字段换掉」,匿名是「谁都认不出这是谁」。 Demo 把姓名、身份证、手机号、邮箱、卡号全换了, 但记录里还留着「1990 年出生」「420502 开头」「每周三来办事」。这三样组合起来,照样能定位到人。 所以正确的顺序是: 先脱敏(去掉直接标识符)→ 再匿名(处理准标识符)→ 最后才谈发布。只做第一步就发出去, 是政务数据事故里最常见的一种。 还有一条容易被忽略:脱敏后的数据要能反向对上业务。 所以不能「随机打乱」,必须保留映射关系——这就是下面要说的「一致性替换」。
二、四类标识符,四套判据
① 身份证。 18 位,末位可能是 X。
判据要注意前后边界:
(?<!\d)(\d{17}[\dXx])(?!\d)——不加边界,19 位流水号的前 18 位也会被当身份证。
② 手机号。 11 位,1[3-9] 开头。这一条几乎不会误伤,因为 11 位且 1 开头的数字很少见。
③ 邮箱。 用 [\w.+-]+@[\w-]+\.[\w.]+ 就够。要注意它可能跨行,所以别用贪婪匹配。
④ 银行卡。 16 到 19 位,必须配 Luhn 校验位。不加校验的话,订单号、流水号、
批次号全会被打码——数据直接没法用了。
Demo 里四类各 20 个,全部识别正确。但这个「全对」有一半功劳是校验位给的。
三、一致性替换:为什么一定要映射表
错误做法是「每遇到一次就生成一个新假名」。 这样同一个人的两条记录会变成两个假名,关联分析直接废掉——而这个分析往往正是你脱敏后要做的。 正确做法是维护一张映射表:原值 → 假名,一对一。Demo 里 83 个原值对应 83 个假名, 83/83 一致。 映射表本身是最敏感的东西:它就是「谁是谁」的答案。所以要单独加密存储、单独授权、单独审计, 绝不能和脱敏后的数据放在一起。 格式上也有讲究:假名要保持原来的形状(身份证换 18 位假身份证、手机号换 11 位假手机号), 否则下游程序的格式校验会全线报错。这是「可用的脱敏」和「只求交差」的分界线。
四、校验位挡不住什么
Demo 里那三条 19 位订单号,Luhn 校验也过了。结果被当成银行卡一起打码,误伤 3 个。 所以「加个校验位就万事大吉」是错的。 校验位只能挡掉「形状像但值无效」的数字,挡不住「形状对且值也合法」的。 真正靠谱的做法是两层:先按字段位置识别(这一列是卡号),再按值校验。只靠正则扫全文, 永远会有误伤和漏网。 动态脱敏的「动态」指的是在查询时才脱,原始数据不动。 它的好处是能按角色脱: 窗口人员看到全脱的,审计人员看到部分脱的,管理员看到原文——同一份数据,三种视图。 代价是每次查询都要跑一遍脱敏逻辑,性能会掉。所以高频查询的表通常用「静态脱敏 + 物化视图」, 低频或敏感查询才用动态脱敏。
五、动手 Demo:脱敏:80 项敏感信息全脱出,3 条订单号被误伤
造 20 条办事记录,每条含身份证、手机号、邮箱、19 位银行卡号; 另加 3 条 19 位订单号,且特意让它们也能通过 Luhn 校验。 用四条正则识别(银行卡配 Luhn 校验),统计每类的识别数、误伤数,以及别名表的一对一一致性。
def luhn(s):
tot, alt = 0, False
for ch in reversed(s):
d = int(ch)
if alt:
d *= 2
if d > 9:
d -= 9
tot += d
alt = not alt
return tot % 10 == 0
pat = {u"身份证": re.compile(r"(?<!\d)(\d{17}[\dXx])(?!\d)"),
u"手机号": re.compile(r"(?<!\d)(1[3-9]\d{9})(?!\d)"),
u"银行卡": re.compile(r"(?<!\d)(\d{19})(?!\d)")}
实跑输出:
--------------------------------------------------------------------
脱敏:80 项敏感信息全脱出,3 条订单号被误伤
--------------------------------------------------------------------
类型 应有 正确识别 完整
------------------------------
身份证 20 20 100%
手机号 20 20 100%
邮箱 20 20 100%
银行卡 20 20 100%
指标 值
--------------------------------
应有敏感项 80 个
正确识别 80 个
召回率 100.0%
误伤(过 Luhn 的订单号) 3 个
别名表大小 83 个
**四类敏感项 80 个,正确识别 80 个,召回 100.0%。**
误伤 3 个:那三条 19 位订单号**连 Luhn 校验都能过**,只凭位数和校验位分不出来。
所以「加个校验位就万事大吉」是错的——校验位只挡掉「形状像但值无效」的数字,
挡不住「形状对且值也合法」的。真正靠谱的做法是**先按字段位置识别,再按值校验**。
别名表 83 个,**同一原值只对应一个别名(83/83 一致)**。
这是「一致性替换」的关键:同一个人在两行里必须换成同一个假名,否则关联分析做不了,
数据就废了。
**但脱敏不等于匿名。** 姓名换了、身份证换了,
「1990 年出生 + 420502 开头 + 每周三来办事」这些还在——组合起来照样能认出人,
这就是上一节 k-匿名要解决的问题。
四类各 20 个, 正确识别 80 个,召回 100%。这个满分不是运气——是因为造数据时每类都按标准格式写的。 真正有价值的是误伤 3 个那一行:三条 19 位订单号连 Luhn 都能过, 位数对、校验位也对,只靠值根本分不出来。 所以生产环境一定要有第二层判据:字段位置。「这一列叫 card_no」比「这串数字像卡号」可靠一万倍。 别名表 83 个、一致性 83/83:同一个原值只映射到一个假名,没有一对多。这是关联分析能做的前提。 注意别名表大小是 83 而不是 80——误伤的那 3 个也进了表。这一点很要紧: 脱敏系统的输出必须能解释「为什么多了三个」。 要说明:这里用正则 + Luhn 做识别, 真实系统还要处理「同一个人身份证换了写法」「手机号带区号」「卡号中间有空格」这些变体。 思路一样,只是规则要更厚。
30 秒实验
- 把银行卡的正则从
\d{19}改成\d{16,19},看重跑结果。 会有更多订单号被误伤——位数放宽,误伤就涨。 - 把 Luhn 校验去掉,看误伤涨到多少。会涨到 20 个以上——这就是「不做校验位」的代价。
- 给三条订单号换成长度 20 位,再跑。它们就不会被银行卡正则匹配到了——长度是第一道闸。
常见误区
- 「删掉姓名就脱敏完了」。年龄、邮编、办事规律组合起来照样能定位人,脱敏之后还要做匿名。
- 「每次随机生成假名更安全」。同一个人的两条记录会变成两个假名,关联分析直接废掉。
- 「有 Luhn 校验就不会误伤」。Demo 里三条订单号连 Luhn 都过了,还要靠字段位置。
自测三问(点开对答案)
1. 脱敏和匿名的区别是什么? —— 脱敏是把敏感字段替换掉;匿名是让谁都认不出这是谁。只做脱敏,准标识符的组合仍能定位到人。 2. 为什么必须维护一对一的映射表? —— 否则同一个人的两条记录会变成两个假名,关联分析做不了。 映射表本身是最敏感的数据,要单独加密和审计。 3. Luhn 校验能挡住什么、挡不住什么? —— 能挡住「形状像但值无效」的数字;挡不住「形状对且值也合法」的,比如恰好合规的订单号。 所以还需要字段位置判据。政务这边的三件事讲完了:隐私、多模态、脱敏。接下来看具体业务——第一个是办事窗口到底该开几个。
- ☐ (本节完,下一节 9.2.1 公共服务优化)