3、数据源层权限实现(SQL层面)
数据源层,说白了就是权限管控的第一道防线。很多团队喜欢在应用层做过滤,但我个人习惯——能下推到数据库层,就别往上提。为什么?因为数据库层过滤是物理级的,你连数据都拿不到,后面再怎么绕也绕不过去。
这一章我们聊三种主流实现方式:WHERE子句动态过滤、视图与存储过程、以及数据库原生的行级安全策略(RLS)。每种方式各有适用场景,我按实战经验给你拆开讲。
3.1 基于WHERE子句的动态过滤
这是最直接、最灵活的方式。你在每条查询后面拼一个条件,比如:
SELECT * FROM sales_order
WHERE region = '华东';
但问题是——用户A只能看华东,用户B只能看华南,你不能写死啊。所以得动态拼。
我在项目中常用的做法是:在报表工具或中间层拿到当前用户的权限上下文,然后注入到SQL里。比如在报表数据集里写:
SELECT * FROM sales_order
WHERE region IN (
SELECT region FROM user_region_permission
WHERE user_id = :current_user_id
);
嗯,这里要注意:千万别把用户ID直接拼字符串。我曾经见过一个项目,开发图省事用字符串拼接,结果被SQL注入搞崩了权限体系。一定要用参数化查询或绑定变量。
核心原则:动态过滤的WHERE条件必须来自权限表,不能来自前端传参。前端传的「用户ID」你信不过。
这种方式的好处是:
- 零侵入——不需要改数据库结构
- 灵活——权限逻辑可以随时调整
- 兼容性好——所有数据库都支持
缺点也很明显:每条SQL都得写权限条件,容易漏。我见过一个报表,20个数据集里有3个忘了加WHERE,结果数据全透出去了……
3.2 视图(View)与存储过程实现行级安全
如果你不想每条SQL都手写权限条件,视图是个好选择。把权限逻辑封装在视图里,用户只能查视图,不能碰基表。
举个例子:
CREATE VIEW v_sales_order AS
SELECT * FROM sales_order
WHERE region IN (
SELECT region FROM user_region_permission
WHERE user_id = SESSION_CONTEXT(N'user_id')
);
这里用了SESSION_CONTEXT——SQL Server里可以在连接时设置会话变量。PostgreSQL里可以用current_setting。用户连上来时,应用层先设置好用户ID,视图自动过滤。
存储过程的方式类似,但更可控:
CREATE PROCEDURE usp_get_sales_order
@user_id INT
AS
BEGIN
SELECT * FROM sales_order
WHERE region IN (
SELECT region FROM user_region_permission
WHERE user_id = @user_id
);
END;
我个人更推荐视图方式。为什么?因为存储过程需要显式调用,而视图对报表工具来说就是一张「表」,拖拽字段时完全无感。我在做Power BI项目时,直接把视图暴露给数据集,权限逻辑完全透明。
避坑指南:视图里如果用了SESSION_CONTEXT,一定要在应用层设置默认值。我曾经遇到一个情况——DBA直接连数据库查视图,没设会话变量,结果视图返回空数据,DBA以为数据丢了……后来我加了个默认值,没设变量时返回空行而不是报错。
3.3 数据库行级安全策略(RLS)介绍
RLS是数据库原生的行级权限方案。PostgreSQL和SQL Server都支持,但实现方式略有不同。
PostgreSQL RLS
PostgreSQL的RLS非常优雅。先开启行级安全,再定义策略:
-- 开启RLS
ALTER TABLE sales_order ENABLE ROW LEVEL SECURITY;
-- 创建策略
CREATE POLICY sales_order_policy ON sales_order
FOR ALL
USING (region IN (
SELECT region FROM user_region_permission
WHERE user_id = current_setting('app.user_id')::INT
));
开启之后,所有对该表的查询都会自动应用策略。用户根本感知不到权限的存在——查到的数据就是他能看的。
我记得有一次帮客户做数据平台,他们之前用视图方案,但视图太多维护成本高。换成RLS后,基表直接暴露给报表工具,权限由数据库自动管控,运维量骤降。
SQL Server RLS
SQL Server的实现叫「行级安全性」,通过内联表值函数实现:
-- 创建过滤函数
CREATE FUNCTION fn_security_predicate(@region NVARCHAR(50))
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS result
WHERE @region IN (
SELECT region FROM user_region_permission
WHERE user_id = CAST(SESSION_CONTEXT(N'user_id') AS INT)
);
-- 绑定安全策略
CREATE SECURITY POLICY sales_order_filter
ADD FILTER PREDICATE dbo.fn_security_predicate(region)
ON dbo.sales_order
WITH (STATE = ON);
两种数据库的RLS核心思路一致:在数据库引擎层面拦截每一行数据,不符合条件的直接过滤掉。应用层、报表层完全不需要改代码。
重要提醒:RLS不是银弹。它会影响查询性能,因为每一行都要过一遍过滤函数。我在一个千万级表上开RLS,查询从200ms涨到800ms。如果数据量极大,建议结合分区或索引优化。
三种方式对比
| 方式 | 维护成本 | 性能影响 | 透明性 | 适用场景 |
|---|---|---|---|---|
| WHERE动态过滤 | 高(每条SQL都要写) | 低 | 低(需应用层配合) | 小团队、快速迭代 |
| 视图/存储过程 | 中(需维护视图层) | 中 | 中(对报表工具透明) | 中大型项目、多报表源 |
| 数据库RLS | 低(一次配置) | 高(每行过滤) | 高(完全透明) | 数据量大、权限规则复杂 |
你想想看,如果团队里报表开发人员多、SQL水平参差不齐,RLS或视图能帮你兜底。如果团队精干、权限规则简单,WHERE动态过滤反而更轻量。
本章知识体系
下面这张图帮你理清三种实现方式的关系和选择路径:
三种方式没有的好坏。我个人的选择逻辑是:先看团队规模,再看数据量,最后看安全等级。小团队用WHERE动态过滤起步,等维护成本高了再切视图或RLS。别一开始就上RLS——性能问题会让你头疼。
我的建议:如果数据库支持RLS(PostgreSQL 9.5+、SQL Server 2016+),优先考虑。虽然性能有损耗,但权限逻辑集中管控,后期审计和排查问题会省很多事。我曾经在一个金融项目里用RLS,审计问「某个用户能看到哪些数据」——直接查策略定义就行,不用翻代码。