如何基于行级安全策略 (RLS) 实现租户隔离?

解读

在国内金融、SaaS、政企多租户场景中,**“租户隔离”**不仅是功能需求,更是等保 2.0、数据出境、个人信息保护法合规的硬门槛。
面试官问“RLS 实现租户隔离”,核心想验证三件事:

  1. 你是否理解 Cloud SQL 只提供引擎级 RLS 能力,而租户标识注入、连接池复用、审计链路必须自己设计;
  2. 能否把 PostgreSQL 原生 RLSGoogle Cloud 身份体系(IAM、Cloud SQL Auth Proxy、VPC-SC) 无缝拼接;
  3. 是否具备高并发、高可用、运维可观测的落地经验,而不是只背语法。

知识点

  1. PostgreSQL RLS 机制:使用 CREATE POLICY 语句,在表级启用 ALTER TABLE ... ENABLE ROW LEVEL SECURITY,通过 USING 表达式控制可见行,WITH CHECK 控制写入行。
  2. 租户标识注入方式
    • Proxy 层注入:Cloud SQL Auth Proxy 支持固定用户名+密码连接,但无法直接携带租户 ID,需借助 SET app.current_tenant = 'xxx'config_param 机制;
    • 连接字符串扩展:在 IAM 数据库身份验证场景下,把租户编码嵌入 IAM 用户 ID(如 tenant_001@project.iam),通过 current_user 解析;
    • JWT Claim 透传:若使用 Cloud SQL 的 IAM 数据库身份验证 + Cloud IAP,可把租户 ID 放在 JWT 自定义声明,通过 current_setting('google.current_claims', true)::json->>'tenant_id' 读取。
  3. 连接池与 RLS 兼容:国内常用 PgBouncer 事务级连接池,必须开启 server_reset_query = DISCARD ALLpgbouncer-auth_query,确保每次归还连接时清理 SET 参数,防止租户串号
  4. 性能与索引:RLS 策略表达式会被内联到查询计划,务必给 租户列建 btree 索引,并把表达式写成 immutable 函数,避免Seq Scan;若租户列是 UUID,使用 hash 索引可减少北京/上海 2 ms 以内的 RTT 抖动。
  5. 审计与合规
    • 开启 pgAudit 扩展,把 pgaudit.log = 'all, -misc' 写入 Google Cloud SQL 标志
    • 通过 Cloud Logging Sinkcloudsql.googleapis.com/postgres.log 路由到 山东/张家口Log Bucket,满足数据不出境要求;
    • 使用 Cloud DLP 对日志中的 身份证号、手机号de-identification,防止个人信息泄露
  6. 高可用与灾备
    • 跨地域只读实例(Beijing, Shanghai, Hong Kong)使用 同一套 RLS 策略,通过 Terraform 模块 google_sql_database_instancedatabase_flags 统一下发;
    • 主实例故障切换后,RLS 策略随 pg_dump 逻辑备份一起恢复,无需人工干预

答案

步骤一:模型层预留租户列

CREATE TABLE orders(
    id          BIGSERIAL PRIMARY KEY,
    tenant_id   VARCHAR(32) NOT NULL,
    amount      NUMERIC(12,2),
    created_at  TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_orders_tenant ON orders(tenant_id);

步骤二:创建 immutable 函数读取当前租户

CREATE OR REPLACE FUNCTION current_tenant() RETURNS text
LANGUAGE sql IMMUTABLE PARALLEL SAFE
AS $$ SELECT current_setting('app.current_tenant', true)::text; $$;

步骤三:启用 RLS 并建策略

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
FOR ALL
USING (tenant_id = current_tenant())
WITH CHECK (tenant_id = current_tenant());

步骤四:应用侧注入租户 ID
Go Cloud SQL Connector 为例:

connStr := "user=svc_user dbname=mydb"
db, _ := sql.Open("cloudsql-postgres", connStr)
// 每次从连接池取连接后,先执行
db.Exec("SET app.current_tenant = $1", tenantIDFromJWT)

步骤五:连接池配置
PgBouncer .ini 关键段:

pool_mode = transaction
server_reset_query = DISCARD ALL

确保事务级池不会复用带租户状态的连接。

步骤六:Terraform 一键下发

resource "google_sql_database_instance" "prod" {
  database_version = "POSTGRES_15"
  settings {
    database_flags {
      name  = "cloudsql.iam_authentication"
      value = "on"
    }
  }
}

RLS 脚本 作为 google_sql_usersql_server_user_details 之外的 initialization_sql 注入,实现GitOps 版本化

步骤七:审计验证
Cloud Logging 查看:

protoPayload.methodName="cloudsql.instances.query"
protoPayload.request.query="SELECT * FROM orders"
protoPayload.request.user="svc_user"
labels.tenant_id="tenant_001"

确保租户字段RLS 过滤一致,满足国内监管现场抽查

拓展思考

  1. 多租户路由到不同 Cloud SQL 实例:当单实例 2 TB/2 万 QPS 到达上限,可结合 Cloud Spanner分片代理(如 Citus on GKE Autopilot),此时 RLS 下沉到分片键,需用 UUIDv7 保证单调递增,避免热点
  2. 实时脱敏:在 RLS 之上再包一层 PostgreSQL 的 security_barrier views,把 手机号、银行卡partial redaction,通过 Cloud DLP API 动态生成视图,满足银保监现场检查
  3. 零信任加固:把 Cloud SQL Auth Proxy 跑在 GKE AutopilotWorkload Identity 池, sidecar 注入 istio-proxy,通过 AuthorizationPolicy 限定只有带 tenant_id header 的 Pod 才能访问 3307 端口,实现南北向+东西向双重隔离
  4. 成本优化:国内 包年包月按需 便宜 37%,对测试租户使用 Cloud SQL 免费试用实例(限 10 GB),通过 Terraform count 动态开关,节省 20% 预算
  5. 灾备演练:每季度用 Google Cloud VMware Engine 在北京私有云拉起 Cloud SQL 备份,验证 RLS 策略跨云恢复后仍生效,满足证监会《证券基金经营机构信息技术管理办法》灾备演练留痕要求。