如何基于行级安全策略 (RLS) 实现租户隔离?
解读
在国内金融、SaaS、政企多租户场景中,**“租户隔离”**不仅是功能需求,更是等保 2.0、数据出境、个人信息保护法合规的硬门槛。
面试官问“RLS 实现租户隔离”,核心想验证三件事:
- 你是否理解 Cloud SQL 只提供引擎级 RLS 能力,而租户标识注入、连接池复用、审计链路必须自己设计;
- 能否把 PostgreSQL 原生 RLS 与 Google Cloud 身份体系(IAM、Cloud SQL Auth Proxy、VPC-SC) 无缝拼接;
- 是否具备高并发、高可用、运维可观测的落地经验,而不是只背语法。
知识点
- PostgreSQL RLS 机制:使用
CREATE POLICY语句,在表级启用ALTER TABLE ... ENABLE ROW LEVEL SECURITY,通过USING表达式控制可见行,WITH CHECK控制写入行。 - 租户标识注入方式:
- 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'读取。
- 连接池与 RLS 兼容:国内常用 PgBouncer 事务级连接池,必须开启 server_reset_query = DISCARD ALL 或 pgbouncer-auth_query,确保每次归还连接时清理
SET参数,防止租户串号。 - 性能与索引:RLS 策略表达式会被内联到查询计划,务必给 租户列建 btree 索引,并把表达式写成 immutable 函数,避免Seq Scan;若租户列是 UUID,使用 hash 索引可减少北京/上海 2 ms 以内的 RTT 抖动。
- 审计与合规:
- 开启 pgAudit 扩展,把
pgaudit.log = 'all, -misc'写入 Google Cloud SQL 标志; - 通过 Cloud Logging Sink 把
cloudsql.googleapis.com/postgres.log路由到 山东/张家口 的 Log Bucket,满足数据不出境要求; - 使用 Cloud DLP 对日志中的 身份证号、手机号 做 de-identification,防止个人信息泄露。
- 开启 pgAudit 扩展,把
- 高可用与灾备:
- 跨地域只读实例(Beijing, Shanghai, Hong Kong)使用 同一套 RLS 策略,通过 Terraform 模块
google_sql_database_instance的database_flags统一下发; - 主实例故障切换后,RLS 策略随 pg_dump 逻辑备份一起恢复,无需人工干预。
- 跨地域只读实例(Beijing, Shanghai, Hong Kong)使用 同一套 RLS 策略,通过 Terraform 模块
答案
步骤一:模型层预留租户列
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_user 的 sql_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 过滤一致,满足国内监管现场抽查。
拓展思考
- 多租户路由到不同 Cloud SQL 实例:当单实例 2 TB/2 万 QPS 到达上限,可结合 Cloud Spanner 或 分片代理(如 Citus on GKE Autopilot),此时 RLS 下沉到分片键,需用 UUIDv7 保证单调递增,避免热点;
- 实时脱敏:在 RLS 之上再包一层 PostgreSQL 的 security_barrier views,把 手机号、银行卡 做 partial redaction,通过 Cloud DLP API 动态生成视图,满足银保监现场检查;
- 零信任加固:把 Cloud SQL Auth Proxy 跑在 GKE Autopilot 的 Workload Identity 池, sidecar 注入 istio-proxy,通过 AuthorizationPolicy 限定只有带 tenant_id header 的 Pod 才能访问 3307 端口,实现南北向+东西向双重隔离;
- 成本优化:国内 包年包月 比 按需 便宜 37%,对测试租户使用 Cloud SQL 免费试用实例(限 10 GB),通过 Terraform count 动态开关,节省 20% 预算;
- 灾备演练:每季度用 Google Cloud VMware Engine 在北京私有云拉起 Cloud SQL 备份,验证 RLS 策略在 跨云恢复后仍生效,满足证监会《证券基金经营机构信息技术管理办法》的灾备演练留痕要求。