ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MikroORM 行级安全(Row Level Security)完整实战指南:PostgreSQL 多租户策略声明、会话上下文与过滤器桥接

MikroORM 行级安全(Row Level Security)完整实战指南:PostgreSQL 多租户策略声明、会话上下文与过滤器桥接 后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载行级安全RLS是 PostgreSQL 将能看哪些行、能写哪些行的边界下沉到数据库本身的安全机制。本文基于 MikroORM 的官方指南 docs/docs/row-level-security.md并结合仓库源码系统讲解如何在实体元数据中声明策略、通过 Schema 生成器与迁移保持策略同步、如何在运行时将会话变量与角色推送到数据库连接以及如何用rls过滤器桥接应用层过滤与数据库策略。读完本文你将能在自己的 MikroORM 项目中落地一套忘记加where tenant_id ?也不会越权的多租户防线。:::info 适用范围仅 PostgreSQL 系驱动 行级安全是 PostgreSQL 特性MikroORM 的该支持仅覆盖postgresql与pglite两个驱动。在其他驱动上声明策略或 RLS 过滤器会在元数据发现阶段直接抛错对应源码见 packages/core/src/platforms/Platform.ts 中的MetadataError.rowLevelSecurityNotSupportedByDriver。 :::为什么需要 RLS把多租户边界交给数据库在传统多租户应用里每个查询都要靠应用代码补一句where tenant_id ?。一旦某个查询漏写了条件、某个em.execute()原生 SQL 绕过 ORM或者团队成员忘记了过滤器数据就可能跨租户泄露。RLS 的核心价值在于边界由数据库强制即使应用层忘记过滤策略也会在数据库侧拒绝越权行。MikroORM 对 RLS 的完整支持链条包括三件事声明在实体元数据中直接声明策略三种实体定义风格均支持同步Schema 生成器与迁移工具自动创建、比对、更新策略 DDL运行时推送把每个请求的会话变量与角色经set_config/set role下发到连接让策略真正生效。官方指南明确强调RLS 是纵深防御defense-in-depth的一层不是应用层 filters 的替代品——两者协同工作而本文后面介绍的 过滤器桥接 可以让一份声明同时驱动两层。在实体上声明策略策略通过policies选项挂到实体上用rowLevelSecurity开关控制表级开关。声明任何策略本身就隐含开启 RLS所以通常只需写policies即可。以下三种声明风格完全等价以多租户文章表为例import { Entity, PrimaryKey, Property } from mikro-orm/decorators/legacy; Entity({ policies: [ { name: article_tenant, using: columns ${columns.tenantId} current_setting(app.tenant)::uuid, check: columns ${columns.tenantId} current_setting(app.tenant)::uuid, }, ], }) export class Article { PrimaryKey() id!: number; Property({ type: uuid }) tenantId!: string; }import { defineEntity, p } from mikro-orm/postgresql; export const Article defineEntity({ name: Article, properties: { id: p.integer().primary(), tenantId: p.uuid(), }, policies: [ { name: article_tenant, using: columns ${columns.tenantId} current_setting(app.tenant)::uuid, check: columns ${columns.tenantId} current_setting(app.tenant)::uuid, }, ], });import { EntitySchema } from mikro-orm/postgresql; export const Article new EntitySchema({ name: Article, properties: { id: { type: number, primary: true }, tenantId: { type: uuid }, }, policies: [ { name: article_tenant, using: columns ${columns.tenantId} current_setting(app.tenant)::uuid, check: columns ${columns.tenantId} current_setting(app.tenant)::uuid, }, ], });Schema 生成器会据此产出如下 DDLalter table article enable row level security; create policy article_tenant on article using (tenant_id current_setting(app.tenant)::uuid) with check (tenant_id current_setting(app.tenant)::uuid);注意using回调中写的是属性名tenantId而生成的 DDL 用的是物理列名tenant_id——这正是回调形式的最大优势见下文。PolicyDef 完整选项PolicyDef的类型定义位于 packages/core/src/typings.ts字段如下选项说明name策略名。省略时按表名 command 冲突后缀自动生成如article_all_policy、article_all_policy_2。command策略作用的命令select、insert、update、delete或默认的all。typepermissive默认与其他 permissive 策略 OR 组合或restrictiveAND 组合——每条 restrictive 策略都必须通过。roles策略适用的数据库角色默认PUBLIC所有角色。usingusing表达式决定哪些已存在的行可见读、更新、删除。checkwith check表达式校验写入的行插入、更新。using / check 的三种写法using与check接受三种形式与 check 约束 同一套机制policies: [ // 1. 回调推荐——引用属性名物理列名由命名策略推导 { name: owner_only, using: columns ${columns.ownerId} current_setting(app.user)::int }, // 2. 纯字符串——原样传入 create policy因此必须使用物理列名 { name: tenant_only, using: tenant_id current_setting(app.tenant)::uuid }, // 3. raw() 片段——同样原样传入但支持参数绑定 { name: published_only, using: raw(status ?, [published]) }, ],优先使用回调列名通常由命名策略Naming Strategy计算得出回调让你引用属性而非手写物理名且日后列重命名时表达式仍然正确。回调签名是(columns, table) string其中columns是属性名到列名的映射。columns对象在三种声明风格中都是完整类型化的defineEntity从属性构建器property builders推断类型Entity()装饰器从被装饰类推断EntitySchema从class选项推断或显式泛型——仅有名字name-only的 schema 退化为无检查的字符串键但运行时映射一致。Permissive 与 Restrictive 的配合permissive 策略之间是OR关系——任意一条放行即放行restrictive 策略之间是AND关系——每条都必须通过。典型组合用 permissive 扩大可见范围用 restrictive 叠加强制约束policies: [ // permissive要么是自己的行要么是共享给你的行 { name: own_rows, using: columns ${columns.ownerId} current_setting(app.user)::int }, { name: shared_rows, using: columns ${columns.isShared} true }, // restrictive无论如何都不得跨租户 { name: tenant_guard, type: restrictive, using: columns ${columns.tenantId} current_setting(app.tenant)::uuid }, ],按命令拆分策略用command拆分读写规则select策略用usinginsert策略用checkupdate两者皆可用policies: [ { name: reader, command: select, roles: [app_reader], using: columns ${columns.tenantId} current_setting(app.tenant)::uuid }, { name: writer, command: insert, roles: [app_writer], check: columns ${columns.tenantId} current_setting(app.tenant)::uuid }, ],create policy reader on ... for select to app_reader using (tenant_id current_setting(app.tenant)::uuid); create policy writer on ... for insert to app_writer with check (tenant_id current_setting(app.tenant)::uuid);三种 RLS 开关状态rowLevelSecurity选项有三种取值行为各异取值行为true无策略开启 RLS 但不授予任何访问——对非 owner 角色是deny-all默认拒绝开关适合在加策略前作为安全默认值force额外发出force row level security对表 owner 也生效默认 owner 与超级用户完全绕过 RLS见 运维注意事项false覆盖策略隐含开启 RLS的默认策略照常创建但 RLS保持关闭处于休眠状态便于先铺策略、后择机切换Entity({ rowLevelSecurity: true }) // 开启无策略 - 非 owner 什么也看不到 Entity({ rowLevelSecurity: force, policies: [/* ... */] }) // owner 也被约束 Entity({ rowLevelSecurity: false, policies: [/* ... */] }) // 策略已创建但 RLS 暂不生效继承规则抽象基类上声明的策略会连同rowLevelSecurity标志一起下传给具体子类子类可声明自己的值覆盖标志单表继承STI策略只能在层级根实体上声明所有子类共享一张表在非根 STI 实体上声明会抛错表级继承TPT策略只保护声明它的那张表——根表的策略只保护根表子表需自行声明。Schema 生成器与迁移策略的自动同步策略被schema:create、schema:update与migration:create自动拾取PostgreSQL 策略也会被反向 introspection保证 diff 干净。比较器packages/sql/src/schema/SchemaComparator.ts按策略名逐一比对新增 / 删除策略→create policy/drop policy。策略按名字匹配因此重命名表现为删旧建新策略不带数据无损开启 / 关闭 RLS→enable/disable row level security以及force/no force其他任何变更using、check、roles、command、type→drop policycreate policy。旧策略在同一 diff 中先于列删除被 drop、后于列新增被重建——策略表达式对其引用列存在依赖原地修改会阻塞删列且 PostgreSQL 本就不支持原地修改策略的for command或as permissive|restrictive。由于目标 schema 是与 PostgreSQL 报告的实际状态比对表达式会被数据库规范化例如in (1, 2, 3)回来变成 any (...)比较器已对此兼容——schema:create后跟一次schema:update往返不产生 drift。迁移快照会序列化完整的策略集合与 RLS 状态对未变化的 schema 再次migration:create会得到空迁移。:::note 无 RLS 的表不产生噪音 不含任何 RLS 的表在迁移快照中完全不输出策略或 RLS 键因此采用该特性不会搅动既有的非 RLS 快照。 :::已有手写策略的数据库如何迁移两条路径如果数据库里已有手工创建原生 SQL 迁移的策略Schema 生成器现在能看见它们——由于它们未镜像到实体元数据任何基于实时 introspection的 diffschema:update或migration:create配合snapshot: false都会建议删掉它们并禁用 RLS。基于快照的migration:create不受影响升级前的快照不含策略状态无从 diff。升级后、运行任何 schema 工具前二选一收编进元数据把已有策略声明到实体上generate-entities会将其往返输出到所有定义风格可直接复制生成的policies数组。声明后 diff 干净ORM 从此接管管理保持不托管设置schemaGenerator: { ignorePolicies: true }。这让 RLS 变成create-only元数据中声明的策略仍会创建、RLS 开启/强制仍会输出但已有策略永远不会被 diff 出 drop/alterRLS 也绝不会被 disable/unforce。唯一例外是列类型变更——PostgreSQL 拒绝在策略引用的列上执行alter column ... type因此未托管策略会被原样从 introspection 得来drop 并在 alter 前后重建。这与ignoreTriggers/ignoreRoutines选项的语义一致。:::cautionsafe模式不是收编策略的手段safe模式会抑制策略 drop 与 RLS 禁用能保护未托管策略免受普通schema:update --safe的伤害——但它不是收编策略的方案非 safe 的运行与迁移仍会删除它们且一条与已声明策略同名的手写策略即使在safe下也会被 drop重建。请使用上面两条路径之一。 :::运行时会话上下文把请求状态推给数据库策略几乎都要读取请求级状态current_setting(app.tenant)或current_user。MikroORM 把这套状态建模为EntityManager 上的会话上下文Session Context——一组会话变量 一个可选角色——并在你的查询执行前下发到 PostgreSQL。SessionContext的类型定义见 packages/core/src/typings.ts核心实现见 packages/core/src/EntityManager.ts。设置与读取// fork 一个专用于该请求/租户的 EM const em orm.em.fork({ session: { variables: { app.tenant: tenantId }, role: app_user, }, }); const articles await em.find(Article, {}); // 只会返回本租户的行// 或在既有上下文中设置/更新 em.setSessionContext({ variables: { app.tenant: tenantId } }); em.setSessionContext({ role: app_user }); const ctx em.getSessionContext(); // { variables: { app.tenant: tenantId }, role: app_user }关键语义setSessionContext合并变量已设置的变量保留提供角色时替换角色clearSessionContext()清空整个上下文没有按 key 删除的接口事务进行中不可变更上下文两种策略下都不行——正在运行的事务永远不会看到该变更因此 ORM 选择fail-closed安全失败直接抛错fork 继承父 EM 的会话上下文向fork()传session会为这个 fork 整体替换它但由setFilterParams为 rls 过滤器 暂存的变量会从拷贝的 filter 参数重新暂存显式session.variables冲突时优先从而保证应用层过滤器与数据库策略保持一致会话变量以字符串应用set_config只接受文本Date值序列化为 ISO 8601因此::timestamptz之类的 cast 可正确解析。两种下发策略sessionContext配置项决定上下文如何到达连接const orm await MikroORM.init({ // ... sessionContext: transaction, // 默认值备选 connection });transaction默认每次begin之后ORM 为每个变量发出select set_config(key, value, true)事务作用域随事务回滚设置角色时发出set local role ...。该模式在 pgBouncertransaction池化模式下安全。事务之外的操作会自动包一层短事务——但仅当确实设置了会话上下文时才包装不用 RLS 时零开销。覆盖em.find/findOne/count、原生insert/nativeUpdate/nativeDelete、upsert、多对多中间表加载、裸em.execute()与 flush// 无需显式事务——ORM 会开一个短事务携带上下文 const em orm.em.fork({ session: { variables: { app.tenant: tenantId } } }); await em.find(Article, {});begin; select set_config($1, $2, true); select ... from article ...; commit;因为em.execute()也被包装不能在事务块内运行的语句vacuum、create index concurrently等在设置了会话上下文时会失败——此时请改用em.getConnection().execute()它从不携带上下文或从一个没有上下文的 EM 执行。嵌套事务savepoint不会重新下发上下文——只在最外层begin设置一次savepoint 继承之。由于上下文只在begin时到达数据库无法发生事务的场景下暂存上下文会被拒绝设置会话上下文时若implicitTransactions: false或经disableTransactions禁用了事务会直接抛错否则写入会无事务执行、静默绕过策略。这类场景请改用connection策略。对应校验逻辑见 packages/core/src/EntityManager.ts。:::caution 流式查询必须包事务 短隐式事务无法存活过一个惰性异步迭代器因此em.stream()/qb.stream()不能被自动包装。为避免静默流出未受策略约束的行ORM 选择fail-closed在transaction策略下事务外对已暂存会话上下文的流式查询直接抛错。请把流式查询包进显式em.transactional()它携带上下文或改用connection策略await em.transactional(async em { for await (const article of em.stream(Article, {})) { // ... } });:::connection在每次保留连接时应用上下文set_config(..., false)与set role ...会话作用域且总是先执行reset all未设置角色时附加reset role确保池化连接不会残留上一次保留时的旧状态。它通过请求上下文解析当前 EM因此与 RequestContext / 中间件方案天然契合。该策略仅限postgresql驱动并与你已有的任何onReserveConnection钩子组合。平台能力开关见 packages/core/src/platforms/Platform.ts 的supportsConnectionSessionContext()。const orm await MikroORM.init({ driver: PostgreSqlDriver, sessionContext: connection, }); await RequestContext.create(orm.em, async () { const em RequestContext.getEntityManager()!; em.setSessionContext({ role: app_user, variables: { app.tenant: tenantId } }); return em.find(Article, {}); });select set_config($1, $2, false); set role app_user; select ... from article ...;:::info 如何选择优先transaction默认每个工作单元自包含pgBouncer transaction 池化下安全。connection会在保留连接的生命周期内设置状态需要 pgBouncersession池化或无外部池化但它免去了隐式事务包装em.stream()也无需额外仪式。 :::connection策略的两个特有问题它通过RequestContext解析活跃 EM——在请求上下文之外显式fork()出的 EM 只会收到连接重置reset all/reset role收不到你的会话变量。请在RequestContext内解析出的 EM 上设置上下文如上面示例每次获取连接时的reset all也会清掉你在onCreateConnection里做的会话级set——请在自己的onReserveConnection钩子在 ORM 的钩子之后运行或逐查询地重新应用。该策略仅postgresql驱动可用——在别处含pglite配置会在 init 时抛错。Fork-per-request 中间件典型多租户模式多租户的标准模式参考 GitHub discussion #6137是从请求中提取租户、为处理器生命周期 fork 一个带作用域的 EMapp.use((req, res, next) { const tenantId getTenantFromRequest(req); // 例如从 JWT claim 或子域名解析 const em orm.em.fork({ session: { variables: { app.tenant: tenantId }, role: app_user, }, }); RequestContext.create(em, next); });此后该请求内的每次em.find、flush 与裸查询都以该租户的会话上下文运行数据库无论如何都强制边界——无论应用代码问什么。写入违规异常被with check策略拒绝的写入会以RowLevelSecurityViolationException抛出ConstraintViolationException的子类由 PostgreSQL SQLSTATE42501映射而来实现在 packages/sql/src/dialects/postgresql/PostgreSqlExceptionConverter.tsimport { RowLevelSecurityViolationException } from mikro-orm/postgresql; const em orm.em.fork({ session: { role: app_user, variables: { app.tenant: tenantA } } }); em.create(Article, { tenantId: tenantB }); // 写错租户 try { await em.flush(); } catch (e) { if (e instanceof RowLevelSecurityViolationException) { // 被策略的 WITH CHECK 拒绝 } }结果缓存与会话上下文会话上下文被自动折叠进 result cache 的缓存键——包括命名缓存键cache: [articles, 5000]设置了上下文时存储为articles|序列化的上下文——因此两个租户发起相同查询绝不会共享缓存结果。一个推论em.clearCache(articles)只会删除无作用域的条目与调用方 EM 上下文对应的条目其他会话上下文作用域的条目靠各自 TTL 过期。相关实现见 packages/core/src/EntityManager.ts。过滤器桥接一份声明、两层强制如果你已经用实体 filter 建模租户隔离可以给过滤器打上rls: true标记让这一份声明同时在两层生效应用层的where与数据库策略。import { defineEntity, p } from mikro-orm/postgresql; export const Order defineEntity({ name: Order, properties: { id: p.integer().primary(), tenantId: p.uuid(), title: p.string(), }, filters: { byTenant: { name: byTenant, cond: args ({ tenantId: args.tenant }), rls: true }, }, });Schema 阶段编译成策略会话变量名与 cast 由属性类型推导create policy order_byTenant_policy on order using (tenant_id current_setting(mikro.byTenant.tenant)::uuid);运行阶段em.setFilterParams同时暂存应用层where子句过滤器启用时与对应的会话变量让数据库策略读到同一个值const em orm.em.fork(); em.setFilterParams(byTenant, { tenant: tenantId }); // 暂存会话变量 { mikro.byTenant.tenant: tenantId } // 过滤器启用时应用层 WHERE 生效... await em.find(Order, {}, { filters: [byTenant] }); // ... where tenant_id ? // ...而绕过应用层过滤器的裸查询仍被策略拦截 await em.execute(select * from order); // 只返回本租户的行一份声明两层强制。会话变量命名与 cast 规则每个被引用的参数映射为一个会话变量mikro.filterName.argName。SQL cast 由被比较属性的类型推导实现在 packages/sql/src/dialects/postgresql/BasePostgreSqlPlatform.ts 的getCurrentSettingCast()列类型Castuuid::uuidbigint::bigintint/smallint/tinyint::intboolean::booleandatetime::timestamptzdate::datetime::timestring/text/enum无current_setting()本身返回文本原生枚举nativeEnumNamecast 到枚举类型本身如::task_status此表之外的类型如decimal无法 cast在 schema 构建时报错。其他约束从 TPT 父类继承的过滤器不会编译到子表——策略只存在于父表上在非根 STI 实体上声明rls过滤器会在发现阶段抛错这类实体没有自己的表策略会静默地永远不被创建单参数过滤器可用rls: { setting: app.current_tenant }覆盖变量名filters: { byTenant: { name: byTenant, cond: args ({ tenantId: args.tenant }), rls: { setting: app.current_tenant } }, },create policy order_byTenant_policy on order using (tenant_id current_setting(app.current_tenant)::uuid);自定义setting且引用多个参数时会抛错——没有单一变量可映射。编译约束静态可分析的条件要编译成静态策略过滤器的cond必须静态可分析。以下情况在 schema 构建时抛出带描述的错误条件中触达em、type、entityName或 find options策略看不到运行时状态async条件参数用在直接比较$eq、$ne、$gt、$gte、$lt、$lte之外——例如{ orgId: { $in: [args.o] } }被比较列的类型无 cast如decimal。对象条件cond: { status: active }与仅基于args的函数都没问题。只有实体作用域的过滤器可以打标——全局config 或addFilter过滤器带rls会被拒绝。:::caution 多个 RLS 过滤器是各自独立的 permissive 策略 每个rls过滤器编译成自己的 permissive 策略且current_setting()不带missing_ok——变量未设置时查询直接报错。这是刻意为之宁可 fail-closed 也不静默返回全部数据。但 permissive 策略是 OR 组合的如果一张表上有多个 RLS 过滤器策略查询会同时被全部评估任何一个变量未设置都会报错。查询此类表前请把每个相关过滤器的参数都暂存好或直接设置变量。若需要容忍未设置变量的策略请用下文 容忍未设置的变量 的nullif模式手写。 :::运维注意事项生产环境前必读表 owner 与超级用户绕过 RLS默认情况下表 owner 角色与超级用户完全绕过 RLS——策略对它们不生效。因此应用必须以非 owner、非超级用户的角色连接Schema 生成器以特权角色连接建表本身不受策略约束这是设计使然若需要 owner 也被约束声明rowLevelSecurity: force。Schema 角色与应用角色分离这里有两个截然不同的角色迁移/Schema 角色拥有表、运行schema:*与迁移是特权角色绕过 RLS应用角色应用运行时连接所用必须非 owner策略才生效且需显式grant所需表权限create role app_user login password ...; grant usage on schema public to app_user; grant select, insert, update, delete on all tables in schema public to app_user; -- 未来新建的表也自动授权 alter default privileges in schema public grant select, insert, update, delete on tables to app_user;然后让应用以app_user连接连接串或user选项若以共享角色连接则每个请求用session: { role: app_user }切换。容忍未设置的变量默认 fail-closed 行为意味着引用current_setting(app.tenant)的策略在app.tenant从未设置时会报错。若你刻意想让策略把未设置变量当作不匹配而非错误给current_setting传第二个参数true返回null而不是抛错并防御空字符串policies: [ { name: tenant_optional, using: columns ${columns.tenantId} nullif(current_setting(app.tenant, true), )::uuid, }, ],对比过滤器桥接的默认行为它刻意省略missing_ok缺失变量是硬错误。:::caution 是空字符串不是未设置 连接池化下未设置比看起来更不可靠。一旦某事务在某物理连接上跑过set_config(app.tenant, ..., true)事务结束后该参数在该连接上仍然已知——其值重置为空字符串而非 undefined。之后的查询若无会话上下文读到的就是而不是抛出unrecognized configuration parameter。只要你的策略对设置值做 cast::uuid、::int……这仍然是 fail-closed——cast 在上会失败。但策略若比较纯文本列且无 cast就会静默匹配列值等于的行而不是报错。租户判别列请优先选择可强 cast 的类型或用上面的nullif(...)守卫让空字符串显式化。 :::索引你的租户列策略在每一行上运行——using表达式本质上是规划器必须满足的额外where子句。请保证策略比较的列tenant_id、owner_id等有索引并保持表达式对索引友好。未索引的租户列会把每个查询变成顺序扫描。pgBouncer 兼容性transaction策略用事务作用域的set_config(..., true)/set local role在 pgBouncertransaction池化模式下安全connection策略在保留连接上设置会话作用域状态因此需要 pgBouncersession池化模式或直连、非池化。在 transaction 模式下使用会把一个请求的上下文泄漏给另一个。没有 PostgreSQL 服务器时如何测试用 sqlite 提速测试在这里行不通——RLS 元数据在任何非 PostgreSQL 驱动上都会在发现阶段抛错这是刻意的静默跳过安全声明会让测试通过却什么都没强制。请改用mikro-orm/pglite驱动它在内存中运行真正的WASMPostgreSQL无需服务器RLS 全链路行为一致——策略 DDL、默认transaction策略下的会话上下文、以及真正的强制生效。两件要注意的事pglite 以超级用户连接和真实服务器一样绕过 RLS——请在测试 setup 中创建普通角色并通过session: { role: ... }切换正如运维注意事项所述connection会话上下文策略在 pglite 上不可用init 时抛错——默认transaction策略已覆盖测试需求。const orm await MikroORM.init({ driver: PgliteDriver, dbName: memory://, entities: [...] }); await orm.schema.refresh(); await orm.em.execute(create role app_user login); await orm.em.execute(grant select, insert, update, delete on all tables in schema public to app_user); const em orm.em.fork({ session: { role: app_user, variables: { app.tenant: tenantId } } }); // 现在策略在你的测试中真正生效如果实在必须留在 sqlite例如大规模既有测试套件暂不迁移逃生通道是discovery.onMetadata钩子——它在校验前运行且能拿到平台一份配置同时适用于生产与测试discovery: { onMetadata: (meta, platform) { if (!platform.supportsRowLevelSecurity()) { meta.policies []; delete meta.rowLevelSecurity; Object.values(meta.filters).forEach(f delete f.rls); } }, },只剥离rls标志能保留过滤器本身因此过滤器桥接用户在这样的测试中仍保有应用层强制——只是数据库层没了。请意识到这意味着什么裸 SQL 与漏掉的过滤器再无防线且会话上下文 APIfork({ session })、setSessionContext在非 PostgreSQL 驱动上仍会抛错直接使用它的代码需要自己的驱动守卫。优先 pglite使用该钩子务必清醒。实体生成器从已有 RLS 数据库回生成从已启用 RLS 的数据库重新生成实体时生成器会在实体上输出显式的policies与rowLevelSecurity标志如实反映 PostgreSQL 报告的状态相关实现见 packages/entity-generator/src/EntityGenerator.ts 与 packages/entity-generator/src/SourceFile.ts。注意由过滤器桥接产生的策略回生成时是普通策略——数据库不存储与源过滤器的关联无法恢复因此你会得到编译后的using/check表达式而非filters: { ..., rls: true }声明。想保留一份声明两层强制的体验请在生成后手工重新引入过滤器。整体工作流参见 Entity Generator 指南。结语一份声明贯穿 Schema、运行时与测试MikroORM 的 RLS 支持把数据库侧强制隔离从手工 SQL 变成了元数据的一等公民policies声明、rowLevelSecurity开关、sessionContext运行时下发以及rls: true过滤器桥接让应用层过滤与数据库策略始终一致。配套的测试用例覆盖了上述全部能力tests/features/rls/rls.postgres.test.ts、tests/features/rls/rls-filter-bridge.postgres.test.ts、tests/features/rls/rls-session-context.postgres.test.ts、tests/features/rls/rls.pglite.test.ts 等可作为理解实现细节与行为边界的活文档。落地时请记住三条红线应用角色必须是非 owner、非超级用户策略比较的列务必建索引测试环境用 pglite 而非 sqlite。做到这三点RLS 就会成为你多租户架构里那道最后、也最可靠的防线。赞分享后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载相关推荐大麦自动抢票工具3 步把整条购票流程自动化大麦自动抢票工具3 步把整条购票流程自动化 你盯着购买按钮手指已经按下去等按下的瞬间却变灰了——热门演出的票往往在开售后几十秒内见底而手动切页面、选场次GUI 自动化RPAPostGraphile v5 安全架构实战用 pgSettings Row Level Security 把授权下沉到 PostgreSQLPostGraphile v5 安全架构实战用 pgSettings Row Level Security 把授权下沉到 PostgreSQL PostG后端API网关elsa-core多租户数据隔离行级安全与过滤elsa core多租户数据隔离行级安全与过滤 多租户Multitenancy架构在SaaS系统中至关重要它允许单一应用实例为多个租户提供服务同时确保后端工作流自动化流程编排低代码上一篇LLMLingua 开源项目教程下一篇开源项目 Alexandria 使用教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表