Relyt One 托管认证预览版发布

为你的应用用户提供注册、登录、会话和组织能力,服务运行在分支旁边,用户就存放在你自己的数据库里。令牌是短时效的 EdDSA JWT,用 JWKS 验证,剩下的交给行级安全(RLS)。

Zhan Lu更新于 2026年10月30日阅读约 7 分钟
本页目录

托管认证现已在 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 上登录,拿到短时效的 JWT 后调用你的 API;API 用 JWKS 验证令牌,并为行级安全设置 app.user_id,而这一切都发生在 relyt_auth 所在的同一个数据库里

为分支启用托管认证后:

  • 分支会获得自己的认证 URL。 你的用户在那里注册和登录。它和你的应用是不同的源:登录表单直接提交到这里,所以你的后端既不会收到密码,也不会存储密码。
  • 用户、会话和组织存储在你的数据库中,位于该分支的 relyt_auth schema 里。它们就是一行行数据,你可以用 SQL 查看,而且就在它们所拥有的那些行旁边。
  • 分支会获得一个 JWKS URL,用于发布其令牌的公钥。你的后端只需要公钥那一半。
  • 认证设置属于分支。 每个分支都有自己的登录方式、来源和密钥,分支或项目被删除时它们也会一并移除。从已启用认证的分支创建的子分支,一开始是父分支的一份副本:它继承可信来源和邮箱设置,其数据库中也有一份父分支终端用户的副本,但会话和签名密钥不会被带过来。

该服务基于 Better Auth 构建,所以如果你用过它,登录接口和会话模型会让你感到很熟悉。

一个请求是如何完成认证的

无论应用使用什么框架,流程都是一样的。

  1. 在认证 URL 上登录。 浏览器把邮箱和密码提交到分支的认证 URL(或者走 GitHub、Google 登录流程),拿回一个会话令牌。
  2. 用会话换取 JWT。 SDK 用会话换取一个有效期 15 分钟、以 EdDSA 签名的 JSON Web Token。它会缓存这个令牌,并在过期前刷新。
  3. 带着 JWT 调用你的 API。 浏览器发送 Authorization: Bearer <jwt>。它自己从不验证令牌。
  4. 在服务端验证。 你的 API 对照分支的 JWKS 检查签名,同时检查签发方(issuer)、受众(audience)和过期时间,并从 sub 中取出用户 ID。
  5. 让 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(停用认证)会停止登录,但不会删除用户、设置或密钥。重新启用后,一切都还是你离开时的样子。

需要留意的一点:BYPASSRLS

只有当你的应用以一个受行级安全约束的角色连接时,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)之前,名称、限制和控制台布局仍可能变化。

试一试

  1. 在控制台中打开一个分支,进入 Auth(认证),选择 Enable auth(启用认证)。
  2. 按照 Relyt One 托管认证入门,为应用加上登录、令牌验证和 RLS。
  3. 或者克隆 TODO 示例,它用个人 TODO 和团队 TODO 实现了上述全部内容,还附带测试套件。

告诉我们哪里出了问题。预览期正是做出改变的时候。

试用托管认证

标签认证Postgres安全

返回博客