行级安全
数据库原生的行级安全(Row-Level Security,RLS)在数据库引擎层面强制执行数据过滤。与数据墙DBW 的数据库策略不同,原生 RLS 的安全边界在数据库内部——即使绕过数据墙DBW 直接连接数据库,过滤仍然生效。数据墙DBW 通过会话上下文将用户声明的身份信息传递给数据库,由数据库的安全策略完成过滤。
IMPORTANT
本章描述的方法使用 SQL Server 原生 RLS。目前仅 SQL Server 支持此功能。
数据库策略 vs 原生 RLS
| 维度 | 数据库策略 | 原生 RLS |
|---|---|---|
| 过滤执行者 | 数据墙DBW 引擎 | SQL Server 引擎 |
| 配置位置 | dab-config.json | SQL Server 安全策略和函数 |
| 绕过 DAB 访问 | 策略不生效 | 策略仍然生效 |
| 跨数据库 | SQL Server、PostgreSQL、MySQL | 仅 SQL Server |
| 灵活性 | 按角色和操作配置 | 按会话上下文配置 |
| 性能 | WHERE 条件追加 | 谓词函数,可建立索引 |
工作原理
1. 数据墙DBW 启用 set-session-context
2. 请求到达,引擎从 JWT 令牌中提取声明
3. 引擎调用 sp_set_session_context 将声明注入 SQL Server 会话
4. 数据库执行查询
5. SQL Server 的安全策略根据 SESSION_CONTEXT 值自动过滤行
6. 过滤后的结果返回给数据墙DBW第一步:创建数据库对象
准备测试表和示例数据:
CREATE TABLE dbo.Revenues (
id INT PRIMARY KEY,
category VARCHAR(100) NOT NULL,
revenue INT,
accessible_role VARCHAR(100) NOT NULL
);
INSERT INTO dbo.Revenues VALUES
(1, 'Book', 5000, 'Oscar'),
(2, 'Comics', 10000, 'Oscar'),
(3, 'Journals', 20000, 'Hannah'),
(4, 'Series', 40000, 'Hannah');在本例中,accessible_role 列存储了可以访问该行的角色名。只有拥有对应角色的用户才能看到该行。
第二步:创建安全谓词函数
创建一个内联表值函数作为安全谓词:
CREATE FUNCTION dbo.RevenuesPredicate(@accessible_role VARCHAR(100))
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS fn_securitypredicate_result
WHERE
@accessible_role = CAST(SESSION_CONTEXT(N'roles') AS VARCHAR(100));| 关键点 | 说明 |
|---|---|
SESSION_CONTEXT(N'roles') | 读取会话上下文中 roles 键的值 |
WITH SCHEMABINDING | 必需——防止底层对象被意外修改,确保安全策略不可绕过 |
| 返回值 | 函数返回 1 表示该行应被包含,不返回表示该行被过滤 |
第三步:创建安全策略
将安全谓词函数绑定到目标表:
CREATE SECURITY POLICY dbo.RevenuesSecurityPolicy
ADD FILTER PREDICATE dbo.RevenuesPredicate(accessible_role)
ON dbo.Revenues;创建后,对该表的所有查询都会自动经过谓词函数过滤。即使是 SELECT * FROM dbo.Revenues 也不会返回全部行——安全策略在数据库引擎层面强制执行。
第四步:配置数据墙DBW
启用会话上下文,将 JWT 声明传递给数据库:
dab init \
--database-type "mssql" \
--connection-string "<connection-string>" \
--set-session-context true或在已有配置中修改:
{
"data-source": {
"database-type": "mssql",
"options": {
"set-session-context": true
}
}
}添加实体:
dab add revenue --source "dbo.Revenues" --permissions "Authenticated:read"第五步:测试行级过滤
启动服务:
dab start以 Oscar 角色请求
curl -H "X-MS-API-ROLE: Oscar" http://localhost:5000/api/revenue只返回 Oscar 可访问的行:
{
"value": [
{ "id": 1, "category": "Book", "revenue": 5000 },
{ "id": 2, "category": "Comics", "revenue": 10000 }
]
}以 Hannah 角色请求
curl -H "X-MS-API-ROLE: Hannah" http://localhost:5000/api/revenue只返回 Hannah 可访问的行:
{
"value": [
{ "id": 3, "category": "Journals", "revenue": 20000 },
{ "id": 4, "category": "Series", "revenue": 40000 }
]
}Authenticated 角色(默认)
curl http://localhost:5000/api/revenue返回空数组——没有任何行的 accessible_role 是 'Authenticated'。
GraphQL 同样生效
curl -X POST http://localhost:5000/graphql \
-H "Content-Type: application/json" \
-H "X-MS-API-ROLE: Oscar" \
-d '{"query": "{ revenues { items { id category revenue } } }"}'原生 RLS 对 REST 和 GraphQL 同时生效,因为过滤发生在数据库引擎层。
引擎实际执行的 SQL
启用了 set-session-context 后,数据墙DBW 在每次查询前执行:
EXEC sp_set_session_context 'roles', 'Oscar', @read_only = 0;
SELECT * FROM dbo.Revenues;roles 是声明键,'Oscar' 是从 JWT 令牌中提取的实际值。安全策略中的 SESSION_CONTEXT(N'roles') 读取到这个值后与表的 accessible_role 列比较,完成过滤。
所有 JWT 声明都会作为键值对传入会话上下文。常见的注入声明包括 roles、sub、userId 等,具体取决于身份提供程序的输出。
常见使用场景
| 场景 | 会话上下文键 | 安全策略逻辑 |
|---|---|---|
| 基于角色的过滤 | roles | 比较用户角色与行的可访问角色 |
| 多租户隔离 | tenantId | 比较用户租户 ID 与行的租户 ID |
| 用户级数据隔离 | sub 或 userId | 比较用户 ID 与行的所有者 ID |
在 SQL 中直接验证
可以用 SQL 验证安全策略是否正确工作:
-- 设置会话上下文
EXEC sp_set_session_context 'roles', 'Oscar';
-- 查询——安全策略自动过滤
SELECT * FROM dbo.Revenues;
-- 只返回 Oscar 的两行
-- 检查当前会话上下文值
SELECT SESSION_CONTEXT(N'roles') AS current_role;
-- 返回 'Oscar'与存储过程配合
安全策略对存储过程中的查询同样生效:
CREATE PROCEDURE dbo.GetRevenuesForCurrentRole
AS
BEGIN
SET NOCOUNT ON;
SELECT id, category, revenue
FROM dbo.Revenues;
END由于安全策略是绑定到表上而不是绑定到查询方式,无论通过直接 SELECT、存储过程还是视图访问 dbo.Revenues,过滤都会执行。
缓存注意事项
WARNING
启用 set-session-context 后,数据墙DBW 会自动禁用该数据源的响应缓存。因为不同用户的会话上下文不同,同一查询的结果因用户而异,缓存结果不能跨用户共享。
对于高流量场景,建议:
- 为谓词函数涉及的列(如
accessible_role)创建索引。 - 评估是否可以用数据墙DBW 数据库策略替代原生 RLS——数据库策略不影响缓存。
- 做性能测试确认安全策略对查询计划的影响。
存储过程使用会话上下文
存储过程可以直接读取 SESSION_CONTEXT 实现自定义逻辑:
CREATE PROCEDURE dbo.GetRevenuesForCurrentRole
AS
BEGIN
DECLARE @role VARCHAR(100) = CAST(SESSION_CONTEXT(N'roles') AS VARCHAR(100));
SELECT id, category, revenue
FROM dbo.Revenues
WHERE accessible_role = @role;
END这种方式依赖存储过程内部的显式 WHERE 过滤,与安全策略的自动过滤互为补充。
故障排查
| 问题 | 检查方法 |
|---|---|
| 没有返回结果 | SELECT * FROM sys.security_policies 确认策略已启用;SELECT SESSION_CONTEXT(N'roles') 确认上下文值正确 |
| 返回了所有行 | 检查安全策略的 STATE 没有设为 OFF;确认谓词函数对匹配行返回 1 |
| 性能下降 | 为谓词列创建索引;临时禁用安全策略对比基线性能 |
| 缓存相关报错 | 这是预期行为——set-session-context 会自动禁用缓存 |
清理
DROP SECURITY POLICY IF EXISTS dbo.RevenuesSecurityPolicy;
DROP FUNCTION IF EXISTS dbo.RevenuesPredicate;
DROP PROCEDURE IF EXISTS dbo.GetRevenuesForCurrentRole;
DROP TABLE IF EXISTS dbo.Revenues;