4. 脱敏规则定义:敏感数据类型识别与规则引擎
脱敏规则这东西,说白了就是整个脱敏工程的"交通法规"。没有它,每个人按自己的想法来,生产数据上去一通乱掩,结果要么脱得不干净,要么把业务数据搞成一团浆糊。我做了这么多年脱敏落地,最大的体会就是:规则定义的质量,直接决定脱敏工程的上限。
这一章我们聚焦两个核心问题——到底哪些数据需要保护,以及怎么让机器自动识别并执行脱敏。你想想看,如果连要保护什么都说不清楚,后面的工程全是白搭。
为什么需要明确的脱敏规则
先讲个我亲身经历的教训。几年前帮一家金融公司做脱敏,开发同学觉得"手机号就是11位数字嘛",写了个简单的正则就上线了。结果呢?业务方反馈说一批400开头的客服热线被误脱了,客户打不进来。原因很简单——规则没区分"手机号"和"固话"。
所以啊,明确的脱敏规则要解决三个问题:
- 一致性——同一类敏感数据,不管出现在哪个系统,脱敏方式必须统一
- 可追溯——每条脱敏记录都能查到是匹配了哪条规则
- 可维护——业务变了、法规变了,改规则配置就行,不用动代码
敏感数据类型识别——先搞清楚你要防什么
识别是第一步,也是最容易出问题的一步。常见的敏感类型就那么几种,但每一种都有它的"脾气"。
| 敏感类型 | 识别规则(正则) | 匹配示例 | 注意点 |
|---|---|---|---|
| 身份证号 | \b\d{17}[\dXx]\b |
110101199001011234 | 15位旧版也要覆盖 |
| 手机号 | \b1[3-9]\d{9}\b |
13800138000 | 排除固话/400号码 |
| 银行卡号 | \b\d{16,19}\b |
6222021234567890 | 需配合Luhn校验 |
| 邮箱 | \b[\w.-]+@[\w.-]+\.\w+\b |
user@example.com | 中文邮箱少但存在 |
| 姓名 | 字典 + 上下文模型 | 张三、李四 | 纯正则误报率极高 |
这里有个坑我要专门提一下:正则不是万能的。比如"姓名"这种类型,你用正则匹配两个中文字符,那"开发"、"测试"全给你匹配上了。我在项目中遇到过这种情况,后来改用"字典树 + 上下文窗口"的方式,准确率才上去。
规则引擎设计——把"判断"和"执行"拆开
规则引擎的核心,说白了就是一个匹配器加一个执行器。但怎么让它们配合得漂亮,这里头有门道。
我个人习惯把规则引擎拆成三层:
- 规则仓库层——存储所有规则定义,支持热加载。我一般用YAML或JSON配置,不写死在代码里。
- 匹配调度层——遍历规则、优先级排序、短路剪枝。性能敏感的场景下,这一步是关键瓶颈。
- 策略执行层——根据匹配结果调用对应的脱敏函数。替换、掩码、加密、截断,每种策略独立实现。
下面这张图把整个链路串了起来,我每次做方案评审都会画一遍:
你看,整个链路里最关键的就是"规则匹配"这个中枢。规则仓库给它喂配置,它告诉执行层用什么策略。我在设计时坚持一个原则:匹配器不碰业务逻辑,执行器不碰识别逻辑。谁越界谁负责,出了问题排查也快。
代码示例:一个轻量级规则引擎
光说不练假把式,我写一个极简但可用的版本。你复制回去改改配置就能用:
from enum import Enum
import re
class SensitiveType(Enum):
ID_CARD = "身份证"
PHONE = "手机号"
BANK_CARD = "银行卡"
EMAIL = "邮箱"
# 识别规则集中配置
RECOGNITION_RULES = {
SensitiveType.ID_CARD: r'\b\d{17}[\dXx]\b',
SensitiveType.PHONE: r'\b1[3-9]\d{9}\b',
SensitiveType.BANK_CARD: r'\b\d{16,19}\b',
SensitiveType.EMAIL: r'\b[\w.-]+@[\w.-]+\.\w+\b',
}
class RuleEngine:
def __init__(self, rules: dict = None):
self.rules = rules or RECOGNITION_RULES
# 预编译正则,性能关键
self._compiled = {k: re.compile(v) for k, v in self.rules.items()}
def identify(self, text: str) -> list:
"""识别文本中包含的所有敏感类型"""
found = []
for sens_type, pattern in self._compiled.items():
if pattern.search(text):
found.append(sens_type)
return found
def mask(self, text: str, strategy: str = "replace") -> str:
"""根据识别结果执行脱敏"""
for sens_type, pattern in self._compiled.items():
if strategy == "replace":
text = pattern.sub("***", text)
elif strategy == "partial":
# 保留前3后4,中间掩码
matches = pattern.findall(text)
for m in matches:
if len(m) > 7:
masked = m[:3] + "****" + m[-4:]
text = text.replace(m, masked, 1)
return text
# 使用示例
engine = RuleEngine()
text = "联系方式:13800138000,身份证号:110101199001011234"
found_types = engine.identify(text)
print(f"识别到: {[t.value for t in found_types]}")
print(f"脱敏后: {engine.mask(text, strategy='partial')}")
设计要点:
· 规则配置化,新增类型不用改引擎代码
· 正则预编译,避免每次匹配都编译一次
· 识别和脱敏分开,方便单独测试
避坑指南——我曾经踩过的几个坑
做规则引擎这几年,摔过的跟头真不少。挑几个典型的说说:
- 规则顺序搞反了——有一次我把"手机号"规则放在"银行卡号"前面,结果一批以"62"开头的银行卡被当手机号处理了。后来我强制要求长规则、高精度规则优先匹配。
- 正则回溯攻击——用户输入的恶意文本能让正则陷入 catastrophic backtracking。我曾经在生产上遇到CPU飙升,排查半天是正则写的太"贪婪"。现在我的正则都加
(?>...)原子分组或超时控制。 - 配置热加载没做版本控制——有次运维同学直接改了线上的规则配置,没通知任何人,结果脱敏效果变了,下游数据同步全错。我后来强制要求规则配置必须走Git + CI,改配置也要审批。
小结
脱敏规则定义这步,急不得。把敏感类型识别清楚,把规则引擎的架构设计好,后面30章的内容才能站得住。我见过太多团队一上来就写脱敏代码,结果做到一半发现"这个类型没考虑到"、"这个规则跟那个规则冲突了",返工成本极高。