应用层的权限控制做得再严密,也挡不住一条直接发往数据库的SQL。只要账号能连库,SELECT * FROM user一句就能把全部手机号和身份证拖走。与其把希望寄托在应用层,不如把防线下沉到数据库本身——用视图把敏感字段封装起来,配好脱敏规则和查询改写,让不同账号看到的同一张表呈现不同的内容。这篇文章就来完整讲讲这套方案的落地思路。

为什么视图能承担数据脱敏的职责
视图的本质是一条被命名存储的SELECT语句,数据库在执行针对视图的查询时,会先把视图定义展开,再与外部查询合并优化。这意味着视图输出的每一列,都可以是任意合法的表达式——你可以用CONCAT截断手机号中间四位,用LEFT只暴露姓氏,甚至用CASE WHEN结合CURRENT_USER()实现“按人显示”的动态脱敏。
相比直接对原始表授权,视图方案的核心优势在于“读写分离到字段级”。原始表t_user只授权给管理员账号,普通应用账号只能访问视图v_user。即使应用代码里写了SELECT *,拿到的也只是脱敏后的数据。这是一种白名单式的防护:不依赖业务代码自觉,安全边界落在数据库的权限体系里。
传统权限控制只能回答“谁能访问哪张表”,而视图脱敏能回答“谁看到的是哪个版本的数据”。对于运营人员要看统计数据、客服要看部分联系方式、开发要排查问题这类场景,往往不需要给全量明文,视图正好补上了这个缺口。
脱敏视图的具体实现
先看原始表结构,假设有一张用户表,包含手机号、身份证号、真实姓名等敏感字段。
CREATE TABLE t_user ( id BIGINT PRIMARY KEY, real_name VARCHAR(50) NOT NULL, mobile CHAR(11) NOT NULL, id_card CHAR(18) NOT NULL, salary DECIMAL(10,2) DEFAULT 0, created_at DATETIME );
脱敏规则通常分几类:手机号保留前三后四,身份证保留前六后四,姓名只留姓,金额可以取整或加噪。下面创建一个面向普通业务账号的脱敏视图。
CREATE VIEW v_user_masked AS SELECT id, CONCAT(LEFT(real_name, 1), '**') AS real_name, CONCAT(LEFT(mobile, 3), '****', RIGHT(mobile, 4)) AS mobile, CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)) AS id_card, ROUND(salary, -3) AS salary, created_at FROM t_user;
然后用数据库自带的权限体系把访问边界锁死:回收普通账号对基表的全部权限,只授予视图的SELECT权限。这一步是整个方案能否生效的关键,如果基表权限没有回收,视图形同虚设。
-- 回收普通账号对基表的权限 REVOKE ALL PRIVILEGES ON mydb.t_user FROM 'app_read'@'%'; -- 只授予视图查询权限 GRANT SELECT ON mydb.v_user_masked TO 'app_read'@'%';
如果想让同一个视图对不同的人显示不同内容,可以在MySQL里借助CURRENT_USER()做条件判断。例如管理员看明文,其他人看脱敏值。
CREATE VIEW v_user_smart AS
SELECT
id,
CASE WHEN CURRENT_USER() LIKE 'admin@%'
THEN real_name
ELSE CONCAT(LEFT(real_name, 1), '**') END AS real_name,
CASE WHEN CURRENT_USER() LIKE 'admin@%'
THEN mobile
ELSE CONCAT(LEFT(mobile, 3), '****', RIGHT(mobile, 4)) END AS mobile,
created_at
FROM t_user;
PostgreSQL的实现思路相同,但能力更强一些。它支持Security Barrier视图,通过WITH (security_barrier)选项阻止使用者在视图谓词上做下推攻击;也可以配合session_user或自定义函数实现行级、列级的差异化输出。此外PostgreSQL还提供了规则系统(Rule)用于查询改写,后面会提到。
查询改写:让业务SQL透明命中脱敏视图
视图建好了,但业务代码里写的是FROM t_user,总不能全量改代码。这就轮到查询改写登场了。查询改写的目标是:业务SQL不改一个字,数据库或者中间层自动把对基表的引用替换成脱敏视图。
第一种方式是同名视图替换。把基表改名成t_user_raw,然后创建一个与原表同名的视图t_user。业务SQL完全无需改动,普通账号查到的自然是脱敏数据。这种做法要特别注意现有DML语句——MySQL的视图默认不可更新,除非满足可更新视图的条件。只读场景用起来最省心。
RENAME TABLE t_user TO t_user_raw; CREATE VIEW t_user AS SELECT id, CONCAT(LEFT(real_name, 1), '**') AS real_name, CONCAT(LEFT(mobile, 3), '****', RIGHT(mobile, 4)) AS mobile, created_at FROM t_user_raw;
第二种方式是PostgreSQL规则改写。利用CREATE RULE或查询重写钩子,在解析阶段把对基表的查询自动重写到目标视图。一些国产数据库和中间件(比如基于Calcite的SQL解析层)也提供类似能力,可以在SQL进入数据库前完成改写。
-- PostgreSQL 规则示例:将对 t_user 的查询重定向到脱敏视图 CREATE RULE rewrite_t_user AS ON SELECT TO t_user DO INSTEAD SELECT * FROM v_user_masked;
第三种方式是代理层改写,在ShardingSphere-Proxy、MyCat这类中间件的SQL解析模块里注册改写规则,拦截到指定表的SELECT语句后做AST级替换。这种方式对数据库零侵入,还能按账号维度下发不同改写规则,适合多套业务共用一个库的场景。改写逻辑示意如下:
// 伪代码:在中间件SQL改写阶段做表名替换
String rewritten = originalSql
.replaceAll("(?i)FROM\\s+t_user\\b", "FROM v_user_masked");
三种方式各有取舍:同名视图最简单但有DML兼容风险;数据库规则性能最好但绑定具体数据库;代理层最灵活但多了一跳链路。中小系统建议先用同名视图,规模大了再上代理层。
方案局限与常见踩坑点
视图脱敏不是银弹,有几个坑必须提前知道。首先是可更新视图的限制:包含函数表达式列的视图,MySQL不允许通过它更新基表。如果业务还需要写入,要么为写入单独开接口走存储过程,要么把脱敏列从更新路径中剥离。
其次是WHERE条件失效问题。脱敏后手机号变成了138****5678,如果业务代码里还有WHERE mobile = '13800005678'这样的查询,走视图必然查不到数据。解决办法是在视图中额外保留一个不可见或者仅限特定账号可见的查询辅助列,或者在改写层把等值查询条件改写到基表列上、只对输出列做脱敏,即“计算下推、输出脱敏”的原则。
再次是聚合与索引问题。对脱敏列做GROUP BY会导致分组失真,比如所有“张**”会被归到一组。同时视图中的函数计算无法利用基表索引,大表上全表扫描会带来性能开销,建议把脱敏字段控制在输出层,过滤和排序尽量走未脱敏的普通列。
最后要强调,视图脱敏和权限控制不是二选一的关系,而是纵深防御的两层。账号权限负责挡住不该进来的人,视图负责让进来的人只看到该看的数据。两层叠加,再配合审计日志记录每一次敏感查询,数据安全的水位才能真正提上来。落地时建议先梳理敏感字段清单,再按角色定义脱敏规则,最后灰度切换视图和权限,避免一次性上线造成业务回归。