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_schema与pg_policies拿到真实的表结构与策略,生成的查询和 RLS 规则更贴合实际,而不是凭空猜测。
与传统模式的简单对比
身份认证对外接口(SDK / 控制台 / 登录方式 / 用户管理 / Token 管理)在两种模式下完全一致——同一份 SDK、同一个控制台入口。功能差异仅在以下维度:
| 维度 | 传统模式 | PG 模式 |
|---|---|---|
| 用户存储位置 | 中心化身份服务 | 数据库 auth.users 表(环境下的 PostgreSQL 内部,可 SQL 直查) |
| 凭证形态 | Refresh Token + Access Token | JWT(Publishable Key / API Key / Access Token,本质都是 JWT) |
| 权限模型 | 角色 + 策略(控制台 JSON DSL) | 三角色(anon / authenticated / service_role)+ GRANT + RLS |
| 用户类型 | 组织成员 / 注册用户 / 匿名用户 | 注册用户 / 匿名用户(无组织架构) |
| 登录 方式 | 全部支持 | 全部支持 |
| SDK 接入 | @cloudbase/js-sdk 的 auth() | 同上 |
| 配置入口 | 控制台「身份认证」 | 控制台「身份认证」 |
核心区别集中在加粗的四个维度:用户存储位置、凭证形态、权限模型、用户类型。其余维度功能等价,仅内部实现不同。
总体架构
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_metadata、user_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 模式下所有请求都会被映射到下列角色之一(取决于请求携带的凭证):
| 角色 | 凭证 | 获取方式 | 特点 |
|---|---|---|---|
anon | Publishable Key 或匿名登录 的 Access Token | 控制台 / YUNAPI 创建 Publishable Key,或调用 /auth/v1/signin/anonymously | JWT 中 role: "anon";可安全嵌入前端 |
authenticated | 登录后颁发的 Access Token | 调用 /auth/v1/signin(账号密码 / 第三方) | JWT 中 role: "authenticated" 且有 sub(用户 ID) |
service_role | API Key | 控制台 / YUNAPI 创建 API Key | JWT 中 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 Key | anon | 前端直接访问,配合 RLS 做匿名能力 | ❌ 不过期 | ✅ 可以安全嵌入前端代码 |
| Access Token(用户登录后颁发) | authenticated | 已登录用户访问数据 | ✅ 受 Token 管理 配置策略约束(默认 2 小时) | 由 SDK 自动管理与刷新 |
| API Key | service_role | 后端管理员操作、批量导入、后台任务 | ❌ 不过期 | ⚠️ 严禁前端,通过环境变量注入 |
Publishable Key 与 API Key 是固定的 JWT Token——网关只读其中的 role 字段。因此它们没有过期时间,直到主动吊销或轮换。
创建 Publishable Key / API Key
两种 Key 均可通过以下方式创建:
- 控制台:登录 云开发控制台 → 环境管理 → API 密钥
- 腾讯云 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_role 为 true) |
meta.platform | Token 来源:PublishableKey / ApiKey / 空(用户登录) |
app_metadata | 系统层面的元数据(登录方式、Provider 等) |
user_metadata | 用户自定义元数据(昵称、头像等) |
在 SQL 中读取登录态
PG 模式预置了 auth schema 下的辅助函数,可以直接在 SQL、RLS 策略、列默认值中使用:
| 函数 | 返回值 | 说明 |
|---|---|---|
auth.jwt() | jsonb | 完整 JWT claims(底层函数),可用于读取 app_metadata、user_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)。anon与authenticated必须通过 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使用 JWTsub作为默认值,不依赖前端传入,从源头杜绝身份伪造。
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_authenticator | PostgREST 网关角色(用户态切换) | NOINHERIT,按 JWT 中 role 字段切换到 anon / authenticated / service_role |
cloudbase_auth_admin | auth schema 管理员 | 拥有 auth 模块全部对象的 ALL WITH GRANT OPTION |
cloudbase_storage_admin | storage schema 管理员 | 拥有 storage 模块全部对象的 ALL WITH GRANT OPTION |
cloudbase_functions_admin | 云函数模块管理员 | 内部使用 |
cloudbase_realtime_admin | Realtime 模块管理员(暂未启用) | 预留 |
cloudbase_dashboard_user | 控制台仪表盘账号 | 拥有 auth / storage 等 schema 的 ALL 权限 |
cloudbase_read_only_user | 只 读审计 | BYPASSRLS,default_transaction_read_only=on,仅 SELECT |
cloudbase_etl_admin | 数据抽取(ETL) | BYPASSRLS,REPLICATION,仅 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=8s、lock_timeout=8s,防止慢查询拖垮实例 - 降级管理员:
cloudbase_postgres虽然拥有 BYPASSRLS,但通过 Event Trigger 机制限制只能执行白名单 DDL(POLICY / INDEX),无法执行CREATE TABLE等结构变更——业务表结构变更需通过云 APIExecutePGSql进行(该 API 默认以更高权限的cloudbase_admin执行)
相关文档
- PostgreSQL 数据库 - 架构与权限模型
- PostgreSQL 数据权限管理(RLS 详解)
- RLS 权限模式库
- Postgres-Native 模式云存储
- 身份认证概述 — 通用身份认证说明
- 管理用户 — 通用用户管理
- Token 管理 — 双令牌、有效期、最大会话数(两种模式通用)
- 权限控制 — 传统模式角色 + 策略