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 中文邮箱少但存在
姓名 字典 + 上下文模型 张三、李四 纯正则误报率极高

这里有个坑我要专门提一下:正则不是万能的。比如"姓名"这种类型,你用正则匹配两个中文字符,那"开发"、"测试"全给你匹配上了。我在项目中遇到过这种情况,后来改用"字典树 + 上下文窗口"的方式,准确率才上去。

我曾经在一个电商项目里只用了正则来识别地址信息,结果把商品详情里的"北京市海淀区"也当成敏感地址给脱了,导致商品描述页面大面积乱码。识别规则一定要加上下文边界判断,别贪图省事。

规则引擎设计——把"判断"和"执行"拆开

规则引擎的核心,说白了就是一个匹配器加一个执行器。但怎么让它们配合得漂亮,这里头有门道。

我个人习惯把规则引擎拆成三层:

  1. 规则仓库层——存储所有规则定义,支持热加载。我一般用YAML或JSON配置,不写死在代码里。
  2. 匹配调度层——遍历规则、优先级排序、短路剪枝。性能敏感的场景下,这一步是关键瓶颈。
  3. 策略执行层——根据匹配结果调用对应的脱敏函数。替换、掩码、加密、截断,每种策略独立实现。

下面这张图把整个链路串了起来,我每次做方案评审都会画一遍:

敏感数据类型识别与规则引擎流程 敏感数据类型识别与规则引擎流程 原始数据输入 敏感类型识别 规则匹配 脱敏策略执行 脱敏数据输出 规则库 配置化管理 核心逻辑:将识别与策略解耦,通过规则引擎灵活编排,避免硬编码

你看,整个链路里最关键的就是"规则匹配"这个中枢。规则仓库给它喂配置,它告诉执行层用什么策略。我在设计时坚持一个原则:匹配器不碰业务逻辑,执行器不碰识别逻辑。谁越界谁负责,出了问题排查也快。

代码示例:一个轻量级规则引擎

光说不练假把式,我写一个极简但可用的版本。你复制回去改改配置就能用:

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章的内容才能站得住。我见过太多团队一上来就写脱敏代码,结果做到一半发现"这个类型没考虑到"、"这个规则跟那个规则冲突了",返工成本极高。