在合规要求日益严格的今天,数据脱敏已经成为后端与数据团队的标配能力。无论是开发联调、数据分析还是对外测试,把手机号、身份证、银行卡这类敏感字段”看不见、改得掉”,既能降低泄露风险,也能满足《个人信息保护法》等监管要求。本文从识别敏感数据讲起,对比静态脱敏与动态脱敏,并给出 Python、SQL 与 PostgreSQL 视图的可落地方案。
一、先认清:什么是数据脱敏
数据脱敏(Data Masking)是指对敏感数据进行变形、替换或遮蔽,使其在保留业务可用性的同时,无法还原出真实信息。它和加密最大的区别在于:加密是可逆的(有密钥就能还原),而脱敏通常是不可逆或单向的,目的是让非授权场景”看不懂、认不出”。
日常生产中常见的高危敏感数据大致分三类:
- 个人身份信息(PII):姓名、身份证号、手机号、家庭住址;
- 支付卡信息(PCI):银行卡号、CVV、有效期;
- 凭据与机密:登录密码、API Token、密钥。
二、第一步:盘点并识别敏感字段
脱敏之前必须先搞清楚”敏感数据到底在哪”。盲目脱敏要么漏掉字段,要么把正常业务字段也改坏。推荐从三个入口入手:
- 数据库字典扫描:根据字段名(phone、mobile、id_card、email、password、card_no)快速定位高危列;
- 数据分类分级表:按业务梳理 PII / PCI / 机密三级,形成公司级资产清单;
- 正则样本扫描:对样本数据跑正则,发现那些字段名”正常”但内容敏感的隐藏数据。
实际落地时,建议把扫描结果沉淀成一张”敏感字段登记表”,标注字段归属系统、敏感级别、脱敏规则与责任人。这张表既是合规审计的凭证,也是后续动态脱敏策略的配置来源,比零散的脚本可靠得多——很多团队脱敏做了却说不清”到底脱了哪些字段”,往往就是缺了这一步沉淀。
-- 通过字段名快速定位高危列(MySQL / PostgreSQL 通用思路)
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'app_db'
AND column_name ~* '(phone|mobile|id_card|idcard|email|password|card|bank)';
import re
patterns = {
"手机": r"1[3-9]\d{9}",
"身份证": r"\d{17}[\dXx]",
"邮箱": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
}
text = "用户 13812345678 邮箱 a@b.com 身份证 110105199003078888"
for name, pat in patterns.items():
print(name, re.findall(pat, text))
字段名扫描只是第一层,真正稳妥的做法是把”加脱敏列 / 建脱敏视图”这类结构变更,纳入在线改表的规范流程,避免直接在业务高峰期动表结构。
三、静态脱敏 vs 动态脱敏
脱敏落地有两条主流路线,选哪条取决于”数据要在什么场景被使用”。
| 维度 | 静态脱敏 | 动态脱敏 |
|---|---|---|
| 执行时机 | 数据导出 / 复制时一次性改写 | 查询时按权限实时遮蔽 |
| 是否改原库 | 否,生成脱敏副本 | 否,原库不动 |
| 适用场景 | 测试库、分析库、外发数据 | 生产查询、客服系统、BI 看板 |
| 可逆性 | 不可逆 | 按角色可逆(授权用户看明文) |
四、静态脱敏实战:导出即改写
最常见的需求是”把生产库导一份脱敏后的数据给测试同学”,让测试环境既真实可用,又不包含真实用户信息。
import re, hashlib
def mask_mobile(v):
return re.sub(r"(\d{3})\d{4}(\d{4})", r"\1****\2", v) if v else v
def mask_email(v):
if not v:
return v
name, domain = v.split("@", 1)
return name[:2] + "***@" + domain
def mask_idcard(v):
return re.sub(r"(\d{6})\d{8}(\w{4})", r"\1********\2", v) if v else v
# 用哈希保留关联性,方便联表测试又不可逆
def hash_keep(v, salt="dev"):
return hashlib.sha256((v + salt).encode()).hexdigest()[:12]
如果更习惯在数据库端处理,也可以直接对脱敏副本库执行 UPDATE(注意:绝不能在原生产库跑):
-- 在脱敏副本库上执行,切勿在生产库运行
UPDATE users
SET mobile = CONCAT(LEFT(mobile,3), '****', RIGHT(mobile,4)),
id_card = CONCAT(LEFT(id_card,6), '********', RIGHT(id_card,4)),
email = CONCAT(LEFT(email,2), '***@', SUBSTRING_INDEX(email,'@',-1));
五、动态脱敏实战:查询时按角色遮蔽
生产环境不能改原库,更适合”谁来看、看什么”的动态策略——同一个字段,客服看到明文,分析师只看到遮蔽后的结果。
-- PostgreSQL:用函数 + 视图实现按角色动态脱敏
CREATE OR REPLACE FUNCTION mask_mobile(v text) RETURNS text AS $$
BEGIN
RETURN CASE WHEN current_user = 'app_ro' THEN v
ELSE REGEXP_REPLACE(v, '(\d{3})\d{4}(\d{4})', '\1****\2') END;
END; $$ LANGUAGE plpgsql;
CREATE VIEW v_users_safe AS
SELECT id, username, mask_mobile(mobile) AS mobile, email
FROM users;
-- 普通分析师只授 v_users_safe 的 SELECT,app_ro 才看明文
GRANT SELECT ON v_users_safe TO analyst;
GRANT SELECT ON users TO app_ro;
关于视图与角色权限的更多基础,可以参考PostgreSQL 与 SQL Server 的差异一文里的权限模型部分。应用层也可以在序列化出口统一脱敏,避免逻辑散落各处:
// 在返回 DTO 之前统一脱敏,集中管理规则
public record UserDTO(Long id, String username, String mobile, String email) {
public static UserDTO from(User u) {
return new UserDTO(u.id(), u.username(),
MaskUtil.mobile(u.mobile()), MaskUtil.email(u.email()));
}
}
六、脱敏不是加密,别混为一谈
很多团队把”脱敏”和”加密”当成一回事,结果要么脱敏不彻底,要么把能还原的加密当成不可逆保护。两者职责完全不同:
| 对比项 | 数据脱敏 | 数据加密 |
|---|---|---|
| 目的 | 让非授权者看不懂 | 让传输 / 存储不可读,可还原 |
| 可逆性 | 通常不可逆 | 有密钥可逆 |
| 典型场景 | 测试、BI、客服展示 | TLS 传输、落盘存储、密码 |
| 合规角色 | 降低泄露面 | 保护数据机密性 |
一句话记住:密码等凭据永远用加密(加盐哈希),而不是脱敏;脱敏解决的是”展示与共享”场景下的可见性问题。
七、生产落地清单
| 序号 | 动作 |
|---|---|
| 1 资产盘点 | 建立敏感字段清单,纳入数据分类分级 |
| 2 环境隔离 | 测试库使用脱敏副本,杜绝直连生产 |
| 3 权限最小化 | 动态脱敏按角色授权,默认遮蔽 |
| 4 日志脱敏 | 打印日志前过滤敏感字段,防”脱敏了库、泄露了日志” |
| 5 回归验证 | 在 CI 中跑脱敏断言,防止代码回退 |
| 6 审计 | 记录谁在何时查看了明文 |
其中”环境隔离”和”回归验证”最适合用工程化手段固化:把脱敏副本库跑在Docker 容器里随取随用,再把脱敏断言接进GitHub Actions,每次发布自动校验敏感字段没有被误输出。对 PostgreSQL 实例本身,也建议配合慢查询优化的监控一起做,避免脱敏函数拖慢查询。
八、小结
数据脱敏不是一段一次性的脚本,而是贯穿”识别—脱敏—授权—审计”的工程能力。静态脱敏解决”数据外发与测试”,动态脱敏解决”生产可见性”,两者配合才能既好用又合规。建议从一张敏感字段清单和一份脱敏副本库开始,再逐步把动态策略接进查询层与日志层,把它变成默认的工程习惯而不是临时救火。




