数据分类分级实战:敏感字段识别与等保落地

数据分类分级是所有合规工作的地基,也是等保 2.0 三级测评里最容易被扣分的环节:制度文件写得漂亮,一问「身份证号存在哪几张表、定了什么级别、谁有权限看」却答不上来。本文给出一套可执行的落地流程,用 SQL 拉元数据、用脚本做内容采样识别敏感字段、用统一的级别映射表驱动管控措施,最终产出可交付的资产台账与分级清单。

适用场景

  • 准备等保 2.0 三级测评,需要提交数据资产清单与分级说明。
  • 业务系统表数量在几十到几百张之间,靠人工翻表结构梳理敏感字段不现实。
  • 需要回答「哪些字段属于个人信息、哪些属于重要数据」以及对应的加密、脱敏、审计要求。
  • 发生过数据外泄事件后,需要明确最小必要授权范围与访问审批链路。

前置条件

  • MySQL 5.7+/8.0 或 PostgreSQL,具备一个只读账号,仅授权 information_schema 与目标库的 SELECT。
  • Python 3.9+,依赖 pymysql;若用 PostgreSQL 换 psycopg2 即可,逻辑不变。
  • 已梳理业务流程,能确认哪些表属于核心交易、哪些属于对外展示。
  • 明确采用的级别定义。本文按四级划分:L1 公开、L2 内部、L3 敏感、L4 核心,可与企业现有制度对齐后改名。

原理说明

分类与分级是两件事。分类回答「这是什么数据」,按业务属性划分,如用户身份、交易订单、设备日志、内容数据;分级回答「泄露后影响多大」,按影响程度划分。同一条数据既有一个分类,也有一个级别,二者组成二维坐标,后续的管控措施由这个坐标决定。

敏感字段识别不能只看字段名。实践中采用三条线索交叉验证:一是元数据线索,字段名或注释命中关键词(id_card、phone、bank);二是内容线索,对字段抽样取值跑正则,命中率超过阈值即判定;三是业务线索,通过访谈确认该字段的实际语义。单用元数据会漏掉 c_no 这种命名,单用内容则会把订单号误判成银行卡号,所以必须交叉。

级别一旦确定,管控措施就要固定下来,避免每张表各自为政。核心原则是:级别决定加密与脱敏强度,分类决定审计粒度。L3/L4 字段在传输与存储上必须加密或脱敏,L4 还要额外做访问审批与操作留痕。

操作步骤

步骤一:建立资产台账表

在独立的合规库里建三张表,分别存资产、字段、分级结果。台账必须可追溯「谁在什么时候定的级」,否则复评时无从下手。

CREATE TABLE dc_asset (
  id            BIGINT PRIMARY KEY AUTO_INCREMENT,
  db_name       VARCHAR(64)  NOT NULL,
  table_name    VARCHAR(64)  NOT NULL,
  table_comment VARCHAR(255),
  biz_domain    VARCHAR(64)  COMMENT '业务分类',
  owner         VARCHAR(64)  COMMENT '数据责任人',
  created_at    DATETIME DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_tbl (db_name, table_name)
);

CREATE TABLE dc_column (
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  asset_id    BIGINT NOT NULL,
  col_name    VARCHAR(64) NOT NULL,
  col_type    VARCHAR(64),
  col_comment VARCHAR(255),
  sample_hit  DECIMAL(5,2) DEFAULT 0 COMMENT '内容命中率%',
  hit_rule    VARCHAR(64)  COMMENT '命中的识别规则',
  level       TINYINT      COMMENT '1公开 2内部 3敏感 4核心',
  reviewed    TINYINT DEFAULT 0,
  KEY idx_asset (asset_id)
);

步骤二:拉取元数据与字段清单

SELECT c.table_schema, c.table_name, t.table_comment,
       c.column_name, c.column_type, c.column_comment
FROM information_schema.columns c
JOIN information_schema.tables t
  ON t.table_schema = c.table_schema AND t.table_name = c.table_name
WHERE c.table_schema NOT IN ('mysql','sys','information_schema','performance_schema')
  AND t.table_type = 'BASE TABLE'
ORDER BY c.table_schema, c.table_name, c.ordinal_position;

这条语句能一次拿到全库表与字段结构,输出到 CSV 后即可作为台账的初始骨架。字段注释为空是常态,不要指望它,识别主要靠下一步的内容采样。

步骤三:内容采样识别敏感字段

脚本对每个字段抽样 200 行(LIMIT 加 ORDER BY RAND() 会全表扫,大表改用主键区间随机),跑规则集计算命中率,命中率 ≥ 60% 判定为该类型。

#!/usr/bin/env python3
import re, pymysql

RULES = {
    "id_card":   (re.compile(r"^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$"), 4),
    "mobile":    (re.compile(r"^1[3-9]\d{9}$"), 3),
    "bank_card": (re.compile(r"^\d{16,19}$"), 4),
    "email":     (re.compile(r"^[\w.+-]+@[\w-]+\.[\w.]+$"), 2),
    "address":   (re.compile(r"[\u4e00-\u9fa5]{2,}(省|市|区|县|路|街|号)"), 2),
}

def luhn(num):
    """银行卡号校验,用于排除订单号、流水号等纯数字串的误报"""
    digits = [int(d) for d in num][::-1]
    total = 0
    for i, d in enumerate(digits):
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return total % 10 == 0

def classify(values):
    best, best_ratio = None, 0.0
    for name, (rx, level) in RULES.items():
        hit = sum(1 for v in values if rx.match(v))
        ratio = hit / len(values) if values else 0.0
        if name == "bank_card":
            hit = sum(1 for v in values if rx.match(v) and luhn(v))
            ratio = hit / len(values) if values else 0.0
        if ratio > best_ratio:
            best, best_ratio = (name, level), ratio
    return (best, best_ratio) if best_ratio >= 0.6 else (None, best_ratio)

conn = pymysql.connect(host="127.0.0.1", user="dc_ro", password="***",
                       charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor)
cur = conn.cursor()

cur.execute("""SELECT table_schema db, table_name tb, column_name col
               FROM information_schema.columns
               WHERE table_schema = 'shop'
               ORDER BY table_name, ordinal_position""")
cols = cur.fetchall()

for c in cols:
    sql = "SELECT `%s` AS v FROM `%s`.`%s` WHERE `%s` IS NOT NULL LIMIT 200" % (
        c["col"], c["db"], c["tb"], c["col"])
    try:
        cur.execute(sql)
        values = [str(r["v"]).strip() for r in cur.fetchall() if str(r["v"]).strip()]
    except Exception as e:
        print("[SKIP]", c["tb"], c["col"], e)
        continue
    if len(values) < 5:
        continue
    (rule, level), ratio = classify(values)
    if rule:
        print("[HIT] %s.%s -> %s L%d %.1f%%" % (c["tb"], c["col"], rule, level, ratio * 100))

两个细节决定结果可信度:正则必须用 ^...$ 全串匹配,否则一段文本里含手机号也会命中,误报率飙升;银行卡号必须过 Luhn 校验,否则 16 位订单号会整片误判为 L4 核心数据,后续管控成本凭空翻倍。

步骤四:定级与人工复核

脚本输出的是建议值,必须人工逐条确认 reviewed=1。定级参考下表,同一字段在不同业务场景下级别可以不同,例如客服可见的手机号是 L3,而出现在对外导出报表中则应视为 L4。

级别 判定标准 典型数据 管控要求
L1 公开 可公开披露,泄露无影响 商品标题、门店地址 无特殊要求
L2 内部 仅内部使用,泄露影响有限 邮箱、部门、内部编号 登录鉴权、禁止外发
L3 敏感 涉及个人信息,泄露造成权益损害 手机号、住址、订单明细 脱敏展示、访问审计、传输加密
L4 核心 泄露造成严重后果或监管处罚 身份证号、银行卡、生物特征 存储加密、动态脱敏、双人审批

步骤五:按级别落地管控措施

先做权限收敛:给应用账号只授予必要字段的查询权,L4 字段单独建视图控制可见性。

-- L4 字段不外露,对外只提供脱敏视图
CREATE VIEW v_user_safe AS
SELECT id, user_name,
       CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)) AS id_card_mask,
       CONCAT(LEFT(mobile, 3), '****', RIGHT(mobile, 4))       AS mobile_mask
FROM t_user;

GRANT SELECT ON shop.v_user_safe TO 'app_read'@'10.0.1.%';
REVOKE SELECT ON shop.t_user FROM 'app_read'@'10.0.1.%';

再做审计留痕:MySQL 开启通用日志会带来明显性能开销,建议只对含 L4 字段的库开启审计插件,或使用 audit_log 按用户过滤。PostgreSQL 侧可用 pgaudit 针对特定表设置 pgaudit.log='read,write'。

最后补导出管控:所有含 L3/L4 字段的导出必须走审批,导出文件加水印与有效期,禁止直接落到个人终端。这一条在测评中常被单独询问,属于「管理制度与技术手段对应」的必查项。

配置验证

  1. 识别准确率抽查:随机抽 20 个被判定为 L3/L4 的字段,人工核对真实数据,命中率应不低于 90%;同时抽 10 个未命中字段,确认没有漏判。
  2. 误报可控:检查 bank_card 规则命中的字段中是否混入订单号、运单号,Luhn 校验后应基本清零。
  3. 权限确实收敛:用应用账号执行 SELECT id_card FROM t_user LIMIT 1; 应报权限错误,而 SELECT * FROM v_user_safe LIMIT 1; 能返回脱敏结果。
  4. 审计能查到:以普通业务账号查询一次敏感表,然后在审计日志中 grep 该账号与表名,应能找到对应记录(含时间、来源 IP、语句)。
  5. 清单完整:资产台账中 reviewed=0 的记录数应为 0,否则说明还有字段未定级,测评时会被直接判定为「未完成梳理」。

常见问题

FAQ 1:一定要分四级吗,三级行不行?

级别数量没有强制统一,关键是与采用的制度对齐并保持内部一致。等保 2.0 与《数据安全法》体系下常用三到五级,中小企业从三级起步完全可行:公开、内部、敏感三级已能覆盖绝大多数管控差异。选四级的理由是「敏感」与「核心」在管控成本上差距明显(是否加密存储、是否双人审批),混在一级会导致 L4 字段被按普通敏感数据处理,属于典型的高风险妥协。

FAQ 2:字段名起得乱七八糟(c_no、f2、col3),识别还有效吗?

有效,但必须完全依赖内容采样而非字段名。本文脚本的判定依据是抽样值的正则命中率,与字段名无关,条件只是该字段确实存了可识别的结构化数据。真正无解的是「混合文本」字段,比如备注里偶尔出现身份证号,这类字段正则命中率达不到 60% 阈值,需要单独做全文扫描:对备注类字段抽样后跑非锚定正则统计出现次数,命中即标记为「可能含敏感信息」,按 L3 处理并要求业务侧整改。

FAQ 3:几百张表、上千万行数据,采样识别会不会把库拖垮?

会,所以必须限流。三条措施:一是只读从库执行,绝不在主库跑;二是控制采样量,每字段 200 行足够计算命中率,不需要全表;三是避开高峰并加 SELECT ... LIMIT 强制截断,大表用主键区间随机(WHERE id BETWEEN ? AND ?)替代 ORDER BY RAND(),后者会触发全表排序。整个扫描建议放在夜间窗口,单次运行控制在 30 分钟内,超时即中断并在次日续跑。

总结

数据分类分级的产出不是一份文档,而是三样可以持续更新的东西:一张覆盖全部字段的资产台账、一套能自动跑的识别脚本、一张级别与管控措施的对应矩阵。落地节奏建议先挑一个核心业务库做试点,把识别准确率与误报率调到位,再横向铺开到其他库。定级完成后必须建立复评机制——新增表上线前先定级、业务变更时同步复核、每年做一次全量回顾。否则台账三个月就与现实脱节,测评时反而成为「制度与实际不符」的证据。