名称: bknd-row-level-security 描述: 在Bknd中实现行级安全(RLS)时使用。涵盖过滤策略、用户所有权模式、公共/私有记录、实体特定RLS、多租户隔离和数据级访问控制。
行级安全(RLS)
使用过滤策略实现数据级访问控制,以限制用户可以访问的记录。
前提条件
- 具有代码优先配置的Bknd项目
- 启用认证(
auth: { enabled: true }) - 启用守卫(
guard: { enabled: true }) - 至少定义一个角色(参见bknd-create-role)
- 具有所有权字段的实体(例如,
user_id)
何时使用UI模式
- 查看当前角色策略
- 快速策略检查
UI步骤: 管理面板 > 认证 > 角色 > 选择角色
注意: RLS配置需要代码模式。UI为只读。
何时使用代码模式
- 实现行级安全
- 创建过滤策略
- 实体特定的数据隔离
- 多租户模式
代码方法
步骤1:向实体添加所有权字段
确保实体具有跟踪所有权的字段:
import { serve } from "bknd/adapter/bun";
import { em, entity, text, number } from "bknd";
const schema = em({
posts: entity("posts", {
title: text().required(),
content: text(),
user_id: number().required(), // 所有权字段
}),
});
步骤2:基本RLS - 仅自己的记录
用户只能读取自己的记录:
serve({
connection: { url: "file:data.db" },
config: {
data: schema.toJSON(),
auth: {
enabled: true,
guard: { enabled: true },
roles: {
user: {
implicit_allow: false,
permissions: [
{
permission: "data.entity.read",
effect: "allow",
policies: [
{
description: "用户仅读取自己的记录",
effect: "filter",
filter: { user_id: "@user.id" },
},
],
},
],
},
},
},
},
});
过滤策略如何工作
| 组件 | 目的 |
|---|---|
effect: "filter" |
应用行级过滤(非允许/拒绝) |
filter |
添加到每个请求的查询条件 |
@user.id |
替换为当前用户ID的变量 |
当ID为5的用户查询帖子时,过滤器转换:
// 用户查询
api.data.readMany("posts", { where: { status: "published" } });
// 变为(应用RLS过滤器后)
api.data.readMany("posts", { where: { status: "published", user_id: 5 } });
步骤3:完整CRUD与RLS
对所有操作应用RLS:
{
roles: {
user: {
implicit_allow: false,
permissions: [
// 读取:自己的记录
{
permission: "data.entity.read",
effect: "allow",
policies: [{
effect: "filter",
filter: { user_id: "@user.id" },
}],
},
// 创建:允许(user_id通过钩子/插件设置)
{ permission: "data.entity.create", effect: "allow" },
// 更新:自己的记录
{
permission: "data.entity.update",
effect: "allow",
policies: [{
effect: "filter",
filter: { user_id: "@user.id" },
}],
},
// 删除:自己的记录
{
permission: "data.entity.delete",
effect: "allow",
policies: [{
effect: "filter",
filter: { user_id: "@user.id" },
}],
},
],
},
},
}
步骤4:实体特定RLS
每个实体不同的RLS规则:
{
roles: {
user: {
implicit_allow: false,
permissions: [
{
permission: "data.entity.read",
effect: "allow",
policies: [
// 帖子:按作者过滤
{
condition: { entity: "posts" },
effect: "filter",
filter: { author_id: "@user.id" },
},
// 评论:按用户过滤
{
condition: { entity: "comments" },
effect: "filter",
filter: { user_id: "@user.id" },
},
// 类别:无过滤(公共)
{
condition: { entity: "categories" },
effect: "allow",
},
],
},
],
},
},
}
步骤5:公共 + 私有记录
用户看到公共记录和自己的私有记录:
{
permissions: [
{
permission: "data.entity.read",
effect: "allow",
policies: [
{
condition: { entity: "posts" },
effect: "filter",
filter: {
$or: [
{ is_public: true }, // 公共帖子
{ user_id: "@user.id" }, // 自己的帖子
],
},
},
],
},
],
}
步骤6:草稿/已发布模式
作者看到自己的草稿,所有人看到已发布的:
{
roles: {
author: {
permissions: [
{
permission: "data.entity.read",
effect: "allow",
policies: [
{
condition: { entity: "posts" },
effect: "filter",
filter: {
$or: [
{ status: "published" }, // 任何人都可以读取已发布的
{ author_id: "@user.id" }, // 作者读取自己的草稿
],
},
},
],
},
],
},
viewer: {
is_default: true,
permissions: [
{
permission: "data.entity.read",
effect: "allow",
policies: [
{
condition: { entity: "posts" },
effect: "filter",
filter: { status: "published" }, // 仅已发布的
},
],
},
],
},
},
}
常见RLS模式
多租户隔离
按组织/租户隔离数据:
const schema = em({
organizations: entity("organizations", {
name: text().required(),
}),
projects: entity("projects", {
name: text().required(),
org_id: number().required(),
}),
tasks: entity("tasks", {
title: text().required(),
org_id: number().required(),
}),
});
// 假设用户有org_id字段
{
roles: {
member: {
permissions: [
{
permission: "data.entity.read",
effect: "allow",
policies: [
{
condition: { entity: { $in: ["projects", "tasks"] } },
effect: "filter",
filter: { org_id: "@user.org_id" },
},
],
},
{
permission: "data.entity.create",
effect: "allow",
policies: [
{
condition: { entity: { $in: ["projects", "tasks"] } },
effect: "allow",
},
],
},
],
},
},
}
基于团队的访问
用户访问属于他们团队的记录:
// 假设用户有team_id字段
{
roles: {
team_member: {
permissions: [
{
permission: "data.entity.read",
effect: "allow",
policies: [{
effect: "filter",
filter: { team_id: "@user.team_id" },
}],
},
{
permission: "data.entity.update",
effect: "allow",
policies: [{
effect: "filter",
filter: { team_id: "@user.team_id" },
}],
},
],
},
},
}
分层访问(经理模式)
经理看到他们下属的数据:
// 经理看到记录,其中:
// - 他们拥有记录,或
// - 记录属于他们管理的人
// 注意:此模式可能需要通过钩子实现自定义逻辑
{
roles: {
manager: {
permissions: [
{
permission: "data.entity.read",
effect: "allow",
policies: [{
effect: "filter",
filter: {
$or: [
{ user_id: "@user.id" },
{ manager_id: "@user.id" },
],
},
}],
},
],
},
},
}
匿名读取,认证写入
{
roles: {
anonymous: {
is_default: true,
permissions: [
{
permission: "data.entity.read",
effect: "allow",
policies: [{
condition: { entity: "posts" },
effect: "filter",
filter: { is_public: true },
}],
},
],
},
user: {
permissions: [
// 读取:公共 + 自己的
{
permission: "data.entity.read",
effect: "allow",
policies: [{
condition: { entity: "posts" },
effect: "filter",
filter: {
$or: [
{ is_public: true },
{ user_id: "@user.id" },
],
},
}],
},
// 创建/更新/删除:仅自己的
{ permission: "data.entity.create", effect: "allow" },
{
permission: "data.entity.update",
effect: "allow",
policies: [{
effect: "filter",
filter: { user_id: "@user.id" },
}],
},
{
permission: "data.entity.delete",
effect: "allow",
policies: [{
effect: "filter",
filter: { user_id: "@user.id" },
}],
},
],
},
},
}
管理员绕过
管理员看到一切,用户看到自己的:
{
roles: {
admin: {
implicit_allow: true, // 不应用RLS过滤器
},
user: {
permissions: [
{
permission: "data.entity.read",
effect: "allow",
policies: [{
effect: "filter",
filter: { user_id: "@user.id" },
}],
},
],
},
},
}
创建时设置用户所有权
RLS过滤器查询结果,但您还需要在创建时设置所有权。
选项1:客户端设置user_id
// 前端代码
const api = new Api({ baseUrl: "http://localhost:7654/api" });
const user = await api.auth.me();
await api.data.createOne("posts", {
title: "我的帖子",
user_id: user.id, // 客户端设置所有权
});
选项2:服务器钩子(推荐)
使用Bknd事件自动设置所有权:
import { serve } from "bknd/adapter/bun";
import { DataRecordMutatingEvent } from "bknd";
serve({
connection: { url: "file:data.db" },
config: {
data: schema.toJSON(),
auth: { /* ... */ },
},
options: {
onBuild: async (app) => {
const events = app.modules.get("events");
events.on(DataRecordMutatingEvent, async (event) => {
if (event.data.action === "create") {
const authModule = app.modules.get("auth");
const user = await authModule.resolveAuthFromRequest(event.data.ctx?.request);
if (user && !event.data.record.user_id) {
event.data.record.user_id = user.id;
}
}
});
},
},
});
验证
1. 创建测试用户
# 用户1
curl -X POST http://localhost:7654/api/auth/password/register \
-H "Content-Type: application/json" \
-d '{"email": "user1@test.com", "password": "pass123"}'
# 用户2
curl -X POST http://localhost:7654/api/auth/password/register \
-H "Content-Type: application/json" \
-d '{"email": "user2@test.com", "password": "pass123"}'
2. 作为用户1创建记录
# 以user1登录
TOKEN1=$(curl -s -X POST http://localhost:7654/api/auth/password/login \
-H "Content-Type: application/json" \
-d '{"email": "user1@test.com", "password": "pass123"}' | jq -r '.token')
# 创建帖子
curl -X POST http://localhost:7654/api/data/posts \
-H "Authorization: Bearer $TOKEN1" \
-H "Content-Type: application/json" \
-d '{"title": "用户1的帖子", "user_id": 1}'
3. 作为用户2验证RLS
# 以user2登录
TOKEN2=$(curl -s -X POST http://localhost:7654/api/auth/password/login \
-H "Content-Type: application/json" \
-d '{"email": "user2@test.com", "password": "pass123"}' | jq -r '.token')
# 查询帖子 - 不应看到user1的帖子
curl http://localhost:7654/api/data/posts \
-H "Authorization: Bearer $TOKEN2"
# 预期:空数组或仅user2的帖子
4. 验证更新RLS
# user2尝试更新user1的帖子 - 应失败或影响0行
curl -X PATCH http://localhost:7654/api/data/posts/1 \
-H "Authorization: Bearer $TOKEN2" \
-H "Content-Type: application/json" \
-d '{"title": "被黑了!"}'
# 预期:404或0受影响(记录被过滤掉)
常见陷阱
过滤器未应用
问题: RLS过滤器未限制数据
修复: 确保守卫已启用:
{
auth: {
enabled: true,
guard: { enabled: true }, // 必需!
},
}
错误的变量占位符
问题: 使用@id而不是@user.id
修复: 使用正确的占位符:
| 占位符 | 含义 |
|---|---|
@user.id |
当前用户的ID |
@user.email |
当前用户的电子邮件 |
@id |
当前记录ID(非用户) |
// 错误 - @id是记录ID,非用户ID
filter: { user_id: "@id" }
// 正确
filter: { user_id: "@user.id" }
缺少实体条件
问题: RLS应用于错误的实体
修复: 为实体特定RLS添加实体条件:
// 错误 - 应用于所有实体
policies: [{
effect: "filter",
filter: { user_id: "@user.id" },
}]
// 正确 - 仅帖子实体
policies: [{
condition: { entity: "posts" },
effect: "filter",
filter: { user_id: "@user.id" },
}]
过滤与允许/拒绝混淆
问题: 当需要过滤时使用effect: "allow"
修复: 理解差异:
| 效果 | 目的 |
|---|---|
allow |
授予权限(无数据过滤) |
deny |
完全阻止权限 |
filter |
允许但过滤结果 |
// 错误 - 允许所有,无过滤
{ effect: "allow", filter: { user_id: "@user.id" } }
// 正确 - 过滤结果
{ effect: "filter", filter: { user_id: "@user.id" } }
创建时未设置所有权
问题: 新记录的user_id为空
修复: 在客户端设置或使用服务器钩子(参见上面的“创建时设置用户所有权”部分)
复杂$or过滤器不工作
问题: $or过滤器返回错误结果
修复: 验证语法:
// 正确的$or语法
filter: {
$or: [
{ is_public: true },
{ user_id: "@user.id" },
],
}
注意事项
应做:
- 向需要RLS的实体添加所有权字段(
user_id) - 使用
effect: "filter"进行行级限制 - 为实体特定规则添加实体条件
- 使用多个用户测试以验证隔离
- 结合RLS与所有权分配钩子
不应做:
- 混淆
@id(记录)与@user.id(用户) - 忘记
guard: { enabled: true } - 将
effect: "allow"与filter字段混合(使用effect: "filter") - 对具有不同所有权字段的实体应用相同的过滤器
- 信任客户端设置所有权而不验证
相关技能
- bknd-create-role - 为RLS定义角色
- bknd-assign-permissions - 配置角色权限
- bknd-protect-endpoint - 保护特定端点
- bknd-public-vs-auth - 公共与认证访问
- bknd-crud-read - 使用过滤器查询数据