列遮蔽原理:列级权限控制、查询改写机制、数据库视图与触发器方案
列遮蔽听起来高大上,说白了就是——同一张表,不同人看不同列。我刚开始接触这个领域时,以为就是简单地在应用层做几个if判断。后来在银行项目里踩了坑,才发现底层原理远比想象中复杂。
今天咱们就把列遮蔽的四大核心原理掰开揉碎讲清楚。你想想看,一个用户查数据库,凭什么他只能看到某些列?这背后其实就两件事:权限怎么管,查询怎么改。
4.1 列级权限控制:从粗粒度到细粒度
传统数据库权限是表级别的——你有权查这张表,就能看到所有列。但现实需求往往是:财务能看到工资列,部门经理只能看到姓名和工号。这就引出了列级权限控制。
核心思想:将权限粒度从表下钻到列,每个用户或角色对每一列都有独立的读/写/不可见权限。
我在项目中遇到过一种典型场景:HR系统里,员工表有20多列。普通员工只能看到自己的基本信息,HR专员能看到全部,部门主管能看到下属的绩效列但看不到薪资列。这种需求用表级权限根本没法做。
列级权限的实现方式主要有三种:
| 实现方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 数据库原生支持 | 如Oracle VPD、SQL Server列级权限 | 性能好,原生集成 | 数据库厂商绑定,语法不通用 |
| 中间件层控制 | 在应用层或代理层拦截SQL | 跨数据库,灵活 | 有性能损耗,维护成本高 |
| 视图/触发器方案 | 通过数据库对象间接实现 | 纯数据库方案,不依赖外部 | 管理复杂,视图爆炸 |
我个人习惯优先使用数据库原生支持,实在不行再考虑中间件。为什么?原生方案走的是数据库内部路径,性能损耗最小。但如果你用的是MySQL或PostgreSQL,原生列级权限支持有限,那就得靠后面几种方案了。
4.2 查询改写机制:SQL的"偷梁换柱"
查询改写,是列遮蔽最核心的"魔法"。用户发来一条SQL,系统在真正执行前把它改掉——该加的列加上,该替换的替换,该过滤的过滤。用户完全感知不到。
举个例子,用户执行:
SELECT * FROM employees WHERE dept_id = 10;
系统根据该用户的列权限,自动改写成:
SELECT emp_id, emp_name, dept_id
FROM employees
WHERE dept_id = 10;
salary列被自动去掉了。用户看到的只有三列,他根本不知道还有salary这回事。
避坑指南:我曾经遇到过一个坑——用户用了SELECT *,改写后列顺序变了,导致应用端按列索引取值全乱套。后来我强制要求:改写后的列顺序必须与原表定义一致,只做减法不做重排。
查询改写的常见策略有:
- 列裁剪:直接删除无权访问的列
- 列替换:将敏感列替换为脱敏函数,如
mask(salary) - 列过滤:在WHERE条件中追加行级过滤
- 列别名:用无意义别名隐藏真实列名
嗯,这里要注意:改写不能破坏SQL语义。比如SELECT *改成显式列名没问题,但如果用户用了ORDER BY 3这种按位置排序的写法,改写后列位置一变就出事了。所以,改写引擎必须同时维护列名和列位置的映射关系。
4.3 数据库视图方案:最经典的"障眼法"
视图方案,说白了就是给不同角色创建不同的"假表"。每个视图只暴露该角色能看的列。用户查视图,就像查真实表一样。
我举个例子,还是那张employees表:
-- 原始表结构
CREATE TABLE employees (
emp_id INT,
emp_name VARCHAR(100),
dept_id INT,
salary DECIMAL(10,2),
id_card VARCHAR(18),
phone VARCHAR(20)
);
-- 为普通员工创建视图
CREATE VIEW v_employees_basic AS
SELECT emp_id, emp_name, dept_id
FROM employees;
-- 为HR创建视图
CREATE VIEW v_employees_hr AS
SELECT emp_id, emp_name, dept_id, salary, phone
FROM employees;
-- 为管理员创建视图
CREATE VIEW v_employees_admin AS
SELECT * FROM employees;
用户登录后,系统根据角色把查询路由到对应的视图上。用户以为自己查的是表,其实查的是视图。
警告:视图方案有个大坑——视图爆炸。如果有10张表、5种角色、3种脱敏级别,你就要创建150个视图。维护起来简直是噩梦。我曾经在一个项目里接手了800多个视图,光理清依赖关系就花了两周。
视图方案的优缺点很明显:
| 维度 | 说明 |
|---|---|
| 优点 | 纯数据库方案,不依赖中间件;性能好(视图可物化);权限管理直观 |
| 缺点 | 视图数量爆炸;DDL变更需同步更新所有视图;不支持动态脱敏(视图是静态的) |
| 适用场景 | 角色少、表结构稳定、脱敏规则固定的场景 |
4.4 触发器方案:动态拦截的"老炮儿"
触发器方案和视图方案思路完全不同。视图是"提前准备好假表",触发器是"在查询执行时动态拦截并修改结果"。
具体做法是:在表上创建INSTEAD OF触发器,拦截SELECT操作,在返回结果前对敏感列进行遮蔽处理。
-- 创建替代触发器(以Oracle语法为例)
CREATE OR REPLACE TRIGGER trg_employees_select
INSTEAD OF SELECT ON employees
FOR EACH ROW
BEGIN
-- 判断当前用户角色
IF SYS_CONTEXT('USERENV', 'SESSION_USER') = 'HR_USER' THEN
-- HR用户看到完整数据
:NEW.salary := :OLD.salary;
:NEW.id_card := :OLD.id_card;
ELSIF SYS_CONTEXT('USERENV', 'SESSION_USER') = 'MGR_USER' THEN
-- 经理看到脱敏后的薪资
:NEW.salary := ROUND(:OLD.salary / 1000) * 1000;
:NEW.id_card := CONCAT(SUBSTR(:OLD.id_card, 1, 4), '********');
ELSE
-- 普通用户看不到敏感列
:NEW.salary := NULL;
:NEW.id_card := NULL;
END IF;
END;
这个方案我实际用过一次,后来再也不敢用了。为什么?性能太差。每查一行数据,触发器就要执行一次逻辑判断。如果表里有100万行,触发器就要跑100万次。而且触发器里做字符串拼接、脱敏计算,CPU直接拉满。
个人经验:触发器方案只适合小表(万级以下)或低频查询场景。我曾经在一个报表系统里用了触发器做列遮蔽,结果月结时跑了40分钟还没出结果,最后被迫切回视图方案。所以,触发器方案慎用,除非你确定数据量不大。
4.5 四种方案对比与选型
讲了这么多,到底该用哪种?我整理了一张对比表,你直接拿去用:
| 方案 | 性能 | 灵活性 | 维护成本 | 适用规模 | 推荐指数 |
|---|---|---|---|---|---|
| 列级权限(原生) | ★★★★★ | ★★☆☆☆ | ★★☆☆☆ | 大 | ★★★★★ |
| 查询改写 | ★★★★☆ | ★★★★★ | ★★★★☆ | 大 | ★★★★☆ |
| 数据库视图 | ★★★★☆ | ★★★☆☆ | ★★★★★ | 中 | ★★★☆☆ |
| 触发器 | ★★☆☆☆ | ★★★★☆ | ★★★☆☆ | 小 | ★★☆☆☆ |
我个人建议的选型思路:
- 大厂、新项目:优先用数据库原生列级权限 + 查询改写引擎
- 中小项目、快速交付:视图方案最稳妥,虽然维护麻烦但不出幺蛾子
- 临时需求、小数据量:触发器方案可以应急,但记得加监控,随时准备切走
4.6 核心逻辑全景图
下面这张图把四种方案的逻辑关系串起来了,你看完应该能对列遮蔽有个整体认知:
这张图把整个流程串起来了。你看,用户发起查询后,先经过权限校验,然后根据选用的方案走不同的处理路径,最终返回的都是已经遮蔽好的结果。用户从头到尾都不知道底层发生了什么。
好了,列遮蔽的四种核心原理就讲到这里。每种方案都有它的脾气,选型时一定要结合你的实际场景——数据量多大、角色有多少、性能要求多高。没有银弹,只有最适合的方案。