Relyt One 托管认证预览版发布
为你的应用用户提供注册、登录、会话和组织能力,服务运行在分支旁边,用户就存放在你自己的数据库里。令牌是短时效的 EdDSA JWT,用 JWKS 验证,剩下的交给行级安全(RLS)。
本页目录
托管认证现已在 Relyt One 上开放预览。为某个分支启用它,这个分支就会拥有一套专属的登录服务,供你的应用用户使用:邮箱和密码、GitHub 和 Google 登录、会话、带角色和权限的组织,以及可以轮换的签名密钥。用户存放在该分支自己数据库中的 relyt_auth schema 里;你的后端对照 JWKS URL 验证短时效的 JWT;Postgres 行级安全则确保一个用户的行不会被另一个用户看到。
启用只需大约一分钟。开源的 @relyt/auth SDK 和一个完整的多用户示例都已发布在 GitHub 上。如果你想直接上手,分步指南见 Relyt One 托管认证入门。
本文讲的是我们为什么要这样设计它。
应用一旦存储任何个人数据,就需要三样东西:让用户登录的方式,让后端知道是谁在调用的方式,以及一个用户永远看不到另一个用户数据的保证。
通常的做法是选一家认证服务商,把它接进应用,然后在应用的整个生命周期里让两套系统保持一致。难点就在这里。
- 你的用户住在别处。 认证服务商有自己的用户表,放在它自己的数据库里,用的是它自己的 ID。你的数据里每一个指向用户的外键,都要跨越系统边界。把「订单」和「用户」关联起来,意味着一次 API 调用或一个同步任务。
- 环境对不上。 你的数据库有生产副本、预发副本,每个 Pull Request 还有一个分支。而认证服务商通常只有一个生产租户,也许再加一个测试租户。结果预览环境要么用生产用户登录,要么对着一个空租户登录,没有任何测试数据能对上。
- 授权只在一个地方执行。 服务商告诉你的 API 调用方是谁。至于这个调用方能不能看某一行,要靠应用代码一条查询一条查询地检查。漏掉一个
WHERE user_id = ...,就是一次数据泄露。
编程智能体让这三点都更加突出。一个搭建新应用的智能体会匆忙接好认证,用的是当时配置的随便哪个租户,权限检查则是想起来哪里就在哪里做。
托管认证的出发点和 Relyt One 的其他部分一样:属于你的数据的东西,就应该和你的数据放在一起。
为分支启用托管认证后:
- 分支会获得自己的认证 URL。 你的用户在那里注册和登录。它和你的应用是不同的源:登录表单直接提交到这里,所以你的后端既不会收到密码,也不会存储密码。
- 用户、会话和组织存储在你的数据库中,位于该分支的
relyt_authschema 里。它们就是一行行数据,你可以用 SQL 查看,而且就在它们所拥有的那些行旁边。 - 分支会获得一个 JWKS URL,用于发布其令牌的公钥。你的后端只需要公钥那一半。
- 认证设置属于分支。 每个分支都有自己的登录方式、来源和密钥,分支或项目被删除时它们也会一并移除。从已启用认证的分支创建的子分支,一开始是父分支的一份副本:它继承可信来源和邮箱设置,其数据库中也有一份父分支终端用户的副本,但会话和签名密钥不会被带过来。
该服务基于 Better Auth 构建,所以如果你用过它,登录接口和会话模型会让你感到很熟悉。
无论应用使用什么框架,流程都是一样的。
- 在认证 URL 上登录。 浏览器把邮箱和密码提交到分支的认证 URL(或者走 GitHub、Google 登录流程),拿回一个会话令牌。
- 用会话换取 JWT。 SDK 用会话换取一个有效期 15 分钟、以 EdDSA 签名的 JSON Web Token。它会缓存这个令牌,并在过期前刷新。
- 带着 JWT 调用你的 API。 浏览器发送
Authorization: Bearer <jwt>。它自己从不验证令牌。 - 在服务端验证。 你的 API 对照分支的 JWKS 检查签名,同时检查签发方(issuer)、受众(audience)和过期时间,并从
sub中取出用户 ID。 - 让 Postgres 来执行。 你的 API 为当前事务设置用户 ID,行级安全策略决定这个用户能看到哪些行。
写成代码,第 4、5 步只有几行:
import { getClaims, RelytAuthUnavailableError } from "@relyt/auth/next";
export async function GET(req: Request) {
let claims;
try {
claims = await getClaims(req, { issuer: process.env.RELYT_AUTH_ISSUER! });
} catch (e) {
// 无法获取公钥:返回 503,免得一次网络抖动就让所有人退出登录
if (e instanceof RelytAuthUnavailableError) return new Response(null, { status: 503 });
throw e;
}
if (!claims) return new Response(null, { status: 401 });
const rows = await db.transaction(async (tx) => {
await tx.execute(sql`select set_config('app.user_id', ${claims.sub}, true)`);
return tx.select().from(notes); // 只有这个用户的笔记
});
return Response.json(rows);
}ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
ALTER TABLE notes FORCE ROW LEVEL SECURITY;
CREATE POLICY own_notes ON notes FOR ALL
USING (owner_id = current_setting('app.user_id', true))
WITH CHECK (owner_id = current_setting('app.user_id', true));这个设置只在当前事务内有效,所以连接池里的连接永远不会把它带到下一个请求。如果代码忘了设置它,current_setting 什么也不返回,策略也就匹配不到任何行。漏掉检查的结果是拒绝访问,而绝不会是数据泄露。
- 邮箱和密码。
- GitHub 和 Google。 把你的 OAuth App 的 Client ID 和 Client Secret 粘贴到控制台即可;Secret 只能写入,之后不会再显示。跨源的社交登录使用一次性交接码配合 PKCE,因此无需第三方 Cookie 也能工作。
认证 URL 只响应来自你所列出的可信来源的浏览器请求,例如 https://app.example.com 或 http://localhost:3000,不支持通配符。社交登录只会重定向回精确注册过的回调 URL。这两个列表都按分支配置,所以预览分支可以信任 https://pr-128.example.dev,而生产环境不必跟着信任它。
- JWT 有效期为 15 分钟,以 EdDSA 签名。SDK 只接受 EdDSA,用其他任何算法签名的令牌都会被拒绝。
- 签名密钥可按需轮换。 轮换后,新密钥会为下一个令牌签名;旧密钥在 JWKS 中保留 960 秒(约 16 分钟),让已签发的令牌仍能通过验证;边缘节点会在约 30 秒内获取新的密钥集。宽限期内最多保留三个旧密钥,因此在宽限期内继续轮换会被拒绝,直到有一个旧密钥过期。验证方在遇到未知的密钥 ID 时应重新获取 JWKS;
@relyt/auth已经这样做了。 - 会话可以撤销,可以针对单个用户,也可以针对分支上的所有人。被撤销的会话无法再刷新,也无法再换取新令牌。已签发的 JWT 在过期前仍然有效,最长 15 分钟,所以如果必须立即中止访问,请在你的应用中也做相应检查。
用户可以创建组织,所有者和管理员可以通过认证 URL 的组织接口邀请成员(预览版不会发送邀请邮件或验证邮件,因此受邀者必须使用已验证的邮箱登录,目前也就是通过 GitHub 或 Google 登录)。每个成员都有一个角色,你可以在控制台中定义自己的资源和操作,例如一个带有 create、read、update 和 delete 操作的 todo 资源,并把它们授权给角色。内置角色有 owner、admin 和 member;你最多可以添加 16 个自定义角色和 32 个自定义资源。
当用户有一个活跃组织时,JWT 会带上它:
"o": {
"id": "…",
"slug": "acme",
"role": "member",
"perms": ["todo:create", "todo:read", "todo:update"]
}你的 API 针对具体操作检查 o.perms,再由一条基于 org_id 的 RLS 策略决定哪些团队行可见。权限列表必须能装进令牌,因此控制台把总量限制在 64 个 resource:action 对、1 KiB 以内;万一装不下,perms 会是 null,这时你的 API 应当拒绝请求,而不是去猜。
- Users(用户)列出分支的终端用户,包括邮箱是否已验证、角色、活跃会话和最近活动时间,并支持在已加载的列表中搜索。
- Integration code(集成代码)展示环境变量,以及 Next.js 中间件、路由处理器、浏览器客户端和社交登录的代码片段,并已填好该分支的 URL。
- Disable auth(停用认证)会停止登录,但不会删除用户、设置或密钥。重新启用后,一切都还是你离开时的样子。
只有当你的应用以一个受行级安全约束的角色连接时,RLS 才能保护你。通过 Relyt One 控制台创建的数据库用户带有 BYPASSRLS,Postgres 会对它们悄无声息地跳过所有策略。任何依赖 RLS 的应用,都请用 SQL 创建一个专用的应用角色:
CREATE ROLE app_user LOGIN NOBYPASSRLS NOSUPERUSER NOCREATEROLE NOCREATEDB;
\password app_user
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON notes TO app_user;\password 是一个 psql 命令,会提示你输入密码,这样密码就不会留在 shell 历史里。然后用 SELECT rolbypassrls FROM pg_roles WHERE rolname = 'app_user'; 检查这个角色,结果应为 f。示例应用的测试套件里有一个用例,专门展示了这一步做错时会发生什么。
托管认证目前是预览版,在我们了解大家如何使用它的过程中,有些功能刻意做得很小,有些暂时缺席:
- 只有组织的所有者和管理员可以启用认证、修改认证设置。
- 子分支不会复制社交登录凭据,而且一个 GitHub OAuth App 只能注册一个回调 URL,所以请每个分支使用一个 OAuth App。
- 暂不发送任何邮件:没有验证邮件、重置密码邮件或邀请邮件。
- 在正式发布(GA)之前,名称、限制和控制台布局仍可能变化。
- 在控制台中打开一个分支,进入 Auth(认证),选择 Enable auth(启用认证)。
- 按照 Relyt One 托管认证入门,为应用加上登录、令牌验证和 RLS。
- 或者克隆 TODO 示例,它用个人 TODO 和团队 TODO 实现了上述全部内容,还附带测试套件。
告诉我们哪里出了问题。预览期正是做出改变的时候。



