跳到主要内容

PG:身份认证

阅读前建议先了解 PG 模式概述,明确该模式与传统模式的整体差异。

本文是 PG 模式环境下身份认证的完整说明,专注介绍其内部工作机制(账号存储、JWT、数据库角色、GRANT / RLS)。通用功能(SDK 接入、登录方式管理、用户信息读写等)与传统模式一致,请参见对应通用文档。如果你使用的是传统模式,本文不适用,请阅读 身份认证概述 与其他章节。

PG 模式下,云开发身份认证 基于 PostgreSQL 构建:账号数据存储在云开发环境的 PostgreSQL 中(位于 auth schema 下),登录态通过 JWT 在请求链路中透传,并被解析到数据库会话变量里,可在 SQL、RLS 策略、列默认值中直接使用。

这种设计带来的好处——账号、表权限、行级权限统一用 SQL 表达,与业务查询同语言、同语义。具体表现为:

  • 账号即是数据库表:账号不再锁在独立的身份服务里,而是落在 PostgreSQL 的 auth.users 等表中,可以像业务表一样用 SQL 增删改查、JOIN、加索引、建外键,无需通过专门的身份服务 API 中转。
  • 权限策略可直接读账号表:因为账号就在 auth.users 表里,RLS 策略可以直接 JOIN 用户的角色、套餐等字段写规则,无需把这些字段冗余到业务表,也无需在应用层先查身份服务再拼 SQL。
  • 登录态 SQL 可见:当前用户 ID、角色等信息在 SQL 中可直接读取(如 auth.uid()),既能用于 RLS 策略,也能作为列默认值实现"自动写入创建者"等模式,不必在业务代码里手工传参。
  • AI 友好:账号、登录态、业务数据、权限策略都以 SQL 形式存在同一个数据库里,AI 编码助手可以直接读 information_schemapg_policies 拿到真实的表结构与策略,生成的查询和 RLS 规则更贴合实际,而不是凭空猜测。

与传统模式的简单对比

身份认证对外接口(SDK / 控制台 / 登录方式 / 用户管理 / Token 管理)在两种模式下完全一致——同一份 SDK、同一个控制台入口。功能差异仅在以下维度:

维度传统模式PG 模式
用户存储位置中心化身份服务数据库 auth.users 表(环境下的 PostgreSQL 内部,可 SQL 直查)
凭证形态Refresh Token + Access TokenJWT(Publishable Key / API Key / Access Token,本质都是 JWT)
权限模型角色 + 策略(控制台 JSON DSL)三角色(anon / authenticated / service_role)+ GRANT + RLS
用户类型组织成员 / 注册用户 / 匿名用户注册用户 / 匿名用户(无组织架构
登录方式全部支持全部支持
SDK 接入@cloudbase/js-sdkauth()同上
配置入口控制台「身份认证」控制台「身份认证」

核心区别集中在加粗的四个维度:用户存储位置、凭证形态、权限模型、用户类型。其余维度功能等价,仅内部实现不同。

总体架构

PG 模式所有请求都遵循同一条「网关 → 数据库」链路:

客户端(Web / 小程序 / 云函数)
│ Authorization: Bearer <Token>
│(Publishable Key / Access Token / API Key —— 三者本质都是 JWT)

CloudBase 网关
│ 解析 JWT → 提取 role / sub / 等 claims
│ 注入到数据库会话变量 request.jwt.claims(JSON 字符串)

PostgREST(自动 RESTful API)
│ 以 cloudbase_authenticator 连接数据库
│ SET ROLE anon | authenticated | service_role (按 JWT 中 role 决定)

PostgreSQL
├─ 第一层:表级权限检查(GRANT ... TO <role>)
├─ 第二层:行级权限过滤(RLS Policy: USING / WITH CHECK)
└─ schemas:
├─ auth 账号系统(users 等)
├─ storage 云存储元数据(buckets / objects)
└─ public 业务表

双重锁定原则:只有表级 GRANT 与 RLS Policy 都通过,操作才会成功。任一层拒绝均返回权限错误。

用户数据存储位置

与传统模式将账号数据存于中心化身份服务不同,PG 模式将账号数据存储在云开发环境下的 PostgreSQL 的 auth schema 中。其中 auth.users 是用户主表,记录用户 ID、邮箱、手机号、登录方式以及 app_metadatauser_metadata 等字段。

auth.users 主键 id 字段类型为 VARCHAR(64),与 JWT 中的 sub claim 完全一致

可以直接在 SQL 中查询、关联用户数据:

-- 查询当前用户总数
SELECT count(*) FROM auth.users;

-- 业务表通过 user_id 关联用户元数据
SELECT o.*, u.user_metadata->>'name' AS buyer_name
FROM public.orders o
JOIN auth.users u ON u.id = o.buyer_id;
控制台同样可用

控制台「用户管理」与 SDK API(auth.currentUser / User.update() 等)使用方式与传统模式完全一致,详见 管理用户。本节着重介绍数据库层面的访问方式。

业务表归属字段建议

业务表归属字段建议保持与 auth.users.id 相同类型(varchar(64),也可使用 text,二者在 PostgreSQL 中可互相赋值),并设置默认值为从 JWT 中读取:

-- 推荐:业务表归属字段统一使用 varchar(64),可选外键引用 auth.users
CREATE TABLE public.orders (
id bigserial PRIMARY KEY,
buyer_id varchar(64) NOT NULL
REFERENCES auth.users(id) -- 可选:建立外键约束
DEFAULT auth.uid(),
-- ...
);

是否建立 REFERENCES auth.users(id) 外键由你自行决定:建立后获得引用完整性,但用户被删除时会被外键阻塞。多数场景下使用纯字段(不建外键)即可。

三种数据库角色

PG 模式下所有请求都会被映射到下列角色之一(取决于请求携带的凭证):

角色凭证获取方式特点
anonPublishable Key 或匿名登录的 Access Token控制台 / YUNAPI 创建 Publishable Key,或调用 /auth/v1/signin/anonymouslyJWT 中 role: "anon";可安全嵌入前端
authenticated登录后颁发的 Access Token调用 /auth/v1/signin(账号密码 / 第三方)JWT 中 role: "authenticated" 且有 sub(用户 ID)
service_roleAPI Key控制台 / YUNAPI 创建 API KeyJWT 中 role: "service_role";具备 BYPASSRLS,严禁暴露前端
危险

API Key(对应 service_role)拥有绕过 RLS 的特权,绝不能暴露在前端代码、小程序、App 中,只能在后端(云函数 / 云托管 / 自建服务)中使用。

与传统模式角色的概念差异

维度传统模式PG 模式
角色定义应用层概念(管理员 / 注册用户 / 匿名用户 / 自定义角色等)PostgreSQL 数据库角色anon / authenticated / service_role
鉴权位置控制台规则引擎数据库内核(GRANT + RLS)
组织成员支持,可按部门 / 自定义角色分配不支持(PG 模式无组织架构)。租户/部门信息可放入 app_metadata 由 RLS 策略读取

凭证与 JWT

PG 模式所有凭证——Publishable Key、API Key、Access Token——本质都是同一种东西:JWT。网关根据 JWT 中的 role 字段决定数据库会话的角色。

三种 Key 对照

Key 类型对应角色用途是否过期安全性
Publishable Keyanon前端直接访问,配合 RLS 做匿名能力❌ 不过期✅ 可以安全嵌入前端代码
Access Token(用户登录后颁发)authenticated已登录用户访问数据✅ 受 Token 管理 配置策略约束(默认 2 小时)由 SDK 自动管理与刷新
API Keyservice_role后端管理员操作、批量导入、后台任务❌ 不过期⚠️ 严禁前端,通过环境变量注入
提示

Publishable Key 与 API Key 是固定的 JWT Token——网关只读其中的 role 字段。因此它们没有过期时间,直到主动吊销或轮换。

创建 Publishable Key / API Key

两种 Key 均可通过以下方式创建:

  1. 控制台:登录 云开发控制台 → 环境管理 → API 密钥
  2. 腾讯云 API(YUNAPI):调用 CreateApiKey 接口
# 创建 Publishable Key
POST https://tcb.tencentcloudapi.com
X-TC-Action: CreateApiKey
X-TC-Version: 2018-06-08
Content-Type: application/json

{ "EnvId": "<envId>", "KeyType": "publish_key" }

# 创建 API Key
{ "EnvId": "<envId>", "KeyType": "api_key" }

authenticated 用户(登录后) JWT 示例

{
"iss": "https://<envId>.api.tcloudbasegateway.com/auth/v1",
"sub": "8d7011e4-2af7-41d7-adb1-a0486499eee3",
"aud": "<envId>",
"exp": 1773559640,
"iat": 1773556040,
"role": "authenticated",
"scope": "openid",
"nonce": "...",
"at_hash": "...",
"name": "ulyssesliu",
"project_id": "<envId>",
"app_metadata": { "provider": "password", "providers": ["password"] },
"user_metadata": { "name": "ulyssesliu" },
"user_type": "external",
"client_type": "client_user",
"is_system_admin": false,
"is_anonymous": false
}

Publishable Key(anon) JWT 示例

{
"iss": "https://<envId>.api.tcloudbasegateway.com/auth/v1",
"sub": "anon",
"aud": "<envId>",
"role": "anon",
"scope":"anonymous",
"name": "Anonymous",
"project_id": "<envId>",
"meta": { "platform": "PublishableKey" },
"app_metadata": { "provider": "anonymous", "providers": ["anonymous"] },
"user_metadata": { "name": "Anonymous" },
"is_anonymous": true
}

注意:Publishable Key 的 sub 字段是固定字符串 "anon",不是某个真实用户 ID;调用 /auth/v1/signin/anonymously 进行匿名登录后获得的 Access Token,role 仍然是 anon会有真实的 sub(匿名用户 ID),可作为业务数据归属字段使用。

API Key(service_role) JWT 示例

{
"aud": "<envId>",
"role": "service_role",
"project_id": "<envId>",
"meta": { "platform": "ApiKey" },
"app_metadata": { "provider": "apikey", "providers": ["apikey"] },
"user_type": "",
"client_type": "client_server",
"is_system_admin": true
}

关键字段含义

字段含义
sub当前用户的 ID(业务表归属字段值来源)。Publishable Key 为固定 "anon";匿名登录有真实 ID
role数据库角色:anon / authenticated / service_role
aud受众,等于 project_id(即 envId)
iss签发方
exp / iat过期 / 签发时间(Unix 时间戳)
is_anonymous是否匿名(Publishable Key 与匿名登录都为 true
is_system_admin是否为系统管理员(service_roletrue
meta.platformToken 来源:PublishableKey / ApiKey / 空(用户登录)
app_metadata系统层面的元数据(登录方式、Provider 等)
user_metadata用户自定义元数据(昵称、头像等)

在 SQL 中读取登录态

PG 模式预置了 auth schema 下的辅助函数,可以直接在 SQL、RLS 策略、列默认值中使用:

函数返回值说明
auth.jwt()jsonb完整 JWT claims(底层函数),可用于读取 app_metadatauser_metadata 等任意字段
auth.uid()text当前用户 ID,等价于 auth.jwt() ->> 'sub'
auth.role()text当前数据库角色(anon / authenticated / service_role),等价于 auth.jwt() ->> 'role'
auth.email()text当前用户邮箱,等价于 auth.jwt() ->> 'email'
-- 当前用户 ID
SELECT auth.uid();

-- 当前角色
SELECT auth.role();

-- 当前用户邮箱
SELECT auth.email();

-- 完整 claims
SELECT auth.jwt();

-- 读取 JWT 中的自定义字段(如租户 ID)
SELECT auth.jwt() -> 'app_metadata' ->> 'tenant_id';
底层原理

网关在转发请求到 PostgREST 前,会把 JWT claims 注入到数据库会话的 GUC 变量 request.jwt.claims(JSON 字符串)。auth.jwt() 本质上就是对该 GUC 变量的封装,auth.uid() / auth.role() / auth.email() 都是 auth.jwt() ->> '<field>' 的语法糖。

预置函数定义如下:

-- 从 JWT 中提取完整 claims(底层函数)
-- 只使用 request.jwt.claims(复数),这是 PostgREST 官方设置的 GUC 键名
CREATE OR REPLACE FUNCTION auth.jwt() RETURNS jsonb
LANGUAGE sql STABLE
AS $$
select
nullif(current_setting('request.jwt.claims', true), '')::jsonb
$$;

-- 从 JWT 中提取用户 ID (sub)(auth.jwt() ->> 'sub' 的语法糖)
CREATE OR REPLACE FUNCTION auth.uid() RETURNS text
LANGUAGE sql STABLE
AS $$
select
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')::text
$$;

-- 从 JWT 中提取 role(auth.jwt() ->> 'role' 的语法糖)
CREATE OR REPLACE FUNCTION auth.role() RETURNS text
LANGUAGE sql STABLE
AS $$
select
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role')::text
$$;

-- 从 JWT 中提取 email(auth.jwt() ->> 'email' 的语法糖)
CREATE OR REPLACE FUNCTION auth.email() RETURNS text
LANGUAGE sql STABLE
AS $$
select
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'email')::text
$$;

直接使用底层 current_setting(...) 写法也可以,但建议优先使用 auth.uid() 等函数——更简洁,也与社区生态(PostgREST / Supabase)惯例一致。

表级权限(GRANT)

通过标准 SQL 控制角色能对表执行哪些操作:

-- 允许匿名用户读取商品表
GRANT SELECT ON public.products TO anon, authenticated;

-- 允许已登录用户写入订单表
GRANT INSERT, SELECT, UPDATE ON public.orders TO authenticated;

-- 序列也需授权(INSERT 时自增 ID)
GRANT USAGE, SELECT ON SEQUENCE public.orders_id_seq TO authenticated;

注意:service_role 默认即可访问所有表(具备 BYPASSRLS)。anonauthenticated 必须通过 GRANT 显式授权才能访问。

行级权限(RLS Policy)

为表启用 RLS 后,所有访问都必须通过策略校验。未被任何 ALLOW 策略匹配的行,默认拒绝访问。

自动绑定当前用户(业务表归属字段最佳实践)

CREATE TABLE public.orders (
id serial PRIMARY KEY,
buyer_id varchar(64) NOT NULL
DEFAULT auth.uid(),
amount numeric(10,2),
created_at timestamptz DEFAULT now()
);

关键:buyer_id 使用 JWT sub 作为默认值,不依赖前端传入,从源头杜绝身份伪造。

SELECT 与 INSERT 策略

-- 启用 RLS
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;

-- 仅允许用户读取自己的订单
CREATE POLICY orders_select_own ON public.orders
FOR SELECT TO authenticated
USING (
buyer_id = auth.uid()
);

-- 仅允许用户写入自己的订单(防伪造)
CREATE POLICY orders_insert_own ON public.orders
FOR INSERT TO authenticated
WITH CHECK (
buyer_id = auth.uid()
);

UPDATE 策略需同时设置 USING 和 WITH CHECK

CREATE POLICY orders_update_own ON public.orders
FOR UPDATE TO authenticated
USING (
buyer_id = auth.uid()
)
WITH CHECK (
buyer_id = auth.uid()
);

USING 控制"能操作哪些行",WITH CHECK 控制"更新后的值是否合法",二者配合防止用户通过 UPDATE 篡改 buyer_id 窃取数据

常见场景示例

公开商品列表 + 私有订单

-- 商品所有人可读
ALTER TABLE public.products ENABLE ROW LEVEL SECURITY;
CREATE POLICY products_public_read ON public.products
FOR SELECT TO anon, authenticated
USING (true);

-- 订单只能看到自己的
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY orders_owner_only ON public.orders
FOR ALL TO authenticated
USING (
buyer_id = auth.uid()
)
WITH CHECK (
buyer_id = auth.uid()
);

多租户 SaaS

-- 业务表带 tenant_id,策略保证用户只能访问自己租户的数据
CREATE POLICY tenant_isolation ON public.records
FOR ALL TO authenticated
USING (
tenant_id = (auth.jwt() -> 'app_metadata' ->> 'tenant_id')
);

更多 RLS 范式(READONLY / PRIVATE / ADMINWRITE / ADMINONLY 等)见 RLS 权限模式库

登录方式与认证 API

登录方式与 SDK 接入方式与传统模式完全一致——同一份 @cloudbase/js-sdk、同一套 auth() 接口、同一份控制台配置。各类登录(匿名 / 用户名密码 / 短信 / 邮箱 / 第三方)请直接参考通用文档 SDK 初始化与登录,开启与关闭见 管理登录方式

PG 模式下也可直接调用 HTTP API 完成登录(更贴近 PostgREST 风格,便于在 SQL 客户端、自建后端或 AI 工具链中使用),相关接口见 /auth/v1/* 系列

云函数中访问数据库

在云函数内有两种访问方式:

方式一:透传用户 access_token(受 RLS 约束)

// 前端调用云函数时,SDK 会自动把登录态透传;云函数可读取请求头中的 access_token
// 云函数内使用该 token 调 REST API,此时以用户身份访问数据库,受 RLS 约束
const res = await fetch(`${REST_BASE}/v1/rdb/rest/orders?select=*`, {
headers: { Authorization: `Bearer ${userAccessToken}` },
});

方式二:使用 API Key(绕过 RLS,管理员身份)

// 仅在云函数、云托管等后端环境使用
// API Key 通常通过环境变量注入
const res = await fetch(`${REST_BASE}/v1/rdb/rest/orders?select=*`, {
headers: { Authorization: `Bearer ${process.env.CLOUDBASE_API_KEY}` },
});
注意

切勿在云函数响应中把 API Key 透传给前端。

附录:系统角色全景

本附录适用于希望深入了解底层实现的开发者。业务开发只需关心 anon / authenticated / service_role 三个角色即可,其他系统角色由平台维护。

PG 模式数据库内部除了三个面向应用的访问角色外,还预置了多个模块化系统角色,用于将不同子系统(auth / storage / functions / realtime / etl 等)的权限做严格隔离:

角色定位关键属性
cloudbase_admin数据库超级管理员SUPERUSER, BYPASSRLS, CREATEROLE, CREATEDB, REPLICATION
cloudbase_postgres应用层最强角色(被 privilege-hardening 收敛)INHERIT, BYPASSRLS,仅保留 CREATE/DROP/ALTER POLICY 与 INDEX 等白名单 DDL
cloudbase_authenticatorPostgREST 网关角色(用户态切换)NOINHERIT,按 JWT 中 role 字段切换到 anon / authenticated / service_role
cloudbase_auth_adminauth schema 管理员拥有 auth 模块全部对象的 ALL WITH GRANT OPTION
cloudbase_storage_adminstorage schema 管理员拥有 storage 模块全部对象的 ALL WITH GRANT OPTION
cloudbase_functions_admin云函数模块管理员内部使用
cloudbase_realtime_adminRealtime 模块管理员(暂未启用)预留
cloudbase_dashboard_user控制台仪表盘账号拥有 auth / storage 等 schema 的 ALL 权限
cloudbase_read_only_user只读审计BYPASSRLSdefault_transaction_read_only=on,仅 SELECT
cloudbase_etl_admin数据抽取(ETL)BYPASSRLSREPLICATION,仅 SELECT
cloudbase_privileged_role特权角色(被授予给若干系统角色)内部使用
anon应用:匿名NOLOGIN NOINHERIT,统一 8 秒 statement_timeout
authenticated应用:已登录NOLOGIN NOINHERIT,统一 8 秒 statement_timeout
service_role应用:管理员NOLOGIN NOINHERIT BYPASSRLS

角色切换链路

客户端请求
│ Authorization: Bearer <JWT>

网关解析 JWT,提取 role 字段


PostgREST 以 cloudbase_authenticator 连接数据库


SET ROLE anon | authenticated | service_role (按 JWT 决定)


执行业务 SQL(受 GRANT + RLS 双层校验)

设计原则

  • 职责分离:每个子系统有独立的 admin 角色,避免一处漏洞影响全局
  • 应用与系统隔离anon / authenticated / service_role 不能直接登录NOLOGIN),只能通过网关切换得到,杜绝直接 SQL 连接绕过权限
  • 超时保护:应用角色统一设置 statement_timeout=8slock_timeout=8s,防止慢查询拖垮实例
  • 降级管理员cloudbase_postgres 虽然拥有 BYPASSRLS,但通过 Event Trigger 机制限制只能执行白名单 DDL(POLICY / INDEX),无法执行 CREATE TABLE 等结构变更——业务表结构变更需通过云 API ExecutePGSql 进行(该 API 默认以更高权限的 cloudbase_admin 执行)

相关文档