← 中文资源

简体中文版本

行级安全

行级安全把租户和归属过滤下沉到数据库,即使应用代码漏写 WHERE 条件,每条查询仍受约束。

阅读英文原文

用策略替代分散过滤

共享数据库中,隔离依赖每条查询都带正确的 WHERE tenant_id = ...。报表端点漏写一次就会泄露数据。行级安全把这个条件移进数据库,对每条语句生效,无论由哪个服务编写。

工作机制

策略把表绑定到针对当前会话求值的表达式。PostgreSQL 中 CREATE POLICY tenant_read ON orders FOR SELECT USING (tenant_id = current_setting('app.tenant_id')::uuid) 限制可见行。应用按事务设置 app.tenant_id,最好用 SET LOCAL,避免连接池把上下文泄漏给下一个请求。

策略默认是 permissive,用 OR 组合;restrictive 策略用 AND 组合。写入需要 WITH CHECK,防止把行插入或移动到其他租户。表属主默认绕过 RLS,除非执行 ALTER TABLE ... FORCE ROW LEVEL SECURITY;超级用户和带 BYPASSRLS 的角色完全不受约束。

何时使用与取舍

多条代码路径访问同一批表、客服工具查询生产库,或合规要求数据库级边界时,RLS 很有价值,并与按租户划分的凭证配合。单租户数据库或只由一个服务访问的表,应用过滤可能已足够。

策略会增加查询规划开销;表达式若不能走索引,还会拖慢查询,因此要为租户列建索引。调试更困难,因为查询会静默少返回行。连接池要求严格重置会话,迁移和管理任务需要显式的绕过路径并记录审计。

示例与常见误解

开发者以应用角色执行 SELECT * FROM orders,只看到当前租户的行;客服工程师以属主身份连接却能看到全部,这正是生产访问不应使用属主角色的原因。

常见误解是执行 ENABLE ROW LEVEL SECURITY 后就自动生效。没有策略时默认拒绝,而没有 FORCE 时属主仍可绕过。相关概念:租户隔离、多租户、SQL 注入。

代码示例

export class TransformError extends Error {
  constructor(message, details) {
    super(message);
    this.name = "TransformError";
    this.index = details.index;
    this.key = details.key ?? null;
    this.raw = details.raw;
  }
}

export async function transformRecords(records, transform, options = {}) {
  const keyField = options.keyField ?? "id";
  if (!Array.isArray(records)) {
    throw new TypeError("records must be an array");
  }
  const out = [];
  for (let index = 0; index < records.length; index++) {
    const raw = records[index];
    if (!raw || typeof raw !== "object") {
      throw new TransformError("record at index " + index + " is not an object", {
        index,
        key: null,
        raw,
      });
    }
    let value;
    try {
      value = await transform(raw, index);
    } catch (err) {
      throw new TransformError(
        "transform failed at index " + index + ": " + String(err.message ?? err),
        { index, key: raw[keyField] ?? null, raw },
      );
    }
    if (value === undefined) {
      throw new TransformError("transform returned undefined at index " + index, {
        index,
        key: raw[keyField] ?? null,
        raw,
      });
    }
    out.push(value);
  }
  return out;
}
-- Supabase RLS policies:run at the Postgres layer
alter table projects enable row level security;

create policy "tenant read own projects"
  on projects for select
  using (tenant_id = auth.jwt() ->> 'tenant_id');

create policy "tenant insert own projects"
  on projects for insert
  with check (tenant_id = auth.jwt() ->> 'tenant_id');

create policy "tenant update own projects"
  on projects for update
  using (tenant_id = auth.jwt() ->> 'tenant_id')
  with check (tenant_id = auth.jwt() ->> 'tenant_id');

-- 服务端绕行角色仅在维护任务中使用
create policy "service role full access"
  on projects for all
  using (auth.role() = 'service_role');