OAuth2 授权服务器
让第三方应用向你的用户申请授权。这和「用 Google 登录你的网站」不是同一件事。
Saavo 内置了 OAuth 2.0 授权服务器,支持你将产品打造成开放平台。它的核心机制是第三方应用引导你的用户登录并授权,然后通过授权码换取访问令牌,从而调用你开放的 API 接口。
注意,这不同于社交账号登录(如 Google 登录)。社交登录是“外部身份进入你的系统”,而 Saavo 解决的是“外部应用访问你的用户资源”。
使用的时候,你需要在那些开放的 API 接口中进行校验,确保请求的 token 权限有效且在允许的授权范围内。
已有功能
模板初始化后,以下功能已经开箱即用:
| 用途 | 默认入口 | 默认状态 |
|---|---|---|
| 授权 | /oauth/authorize | oauth2.enableService 默认为 true |
| 登录后继续 | /oauth/authorize/continue | 已启用 |
| 同意页 | /oauth/authorize/consent | 已启用 |
| 换取令牌 | POST /oauth/token | 已挂载 |
| 撤销令牌 | POST /oauth/token/revoke | 已挂载 |
| 获取授权后的用户信息 | GET /oauth/current-user | 需要 user scope |
| 管理第三方客户端 | /dashboard/oauth-server | 管理员可创建客户端 |
该模板默认支持授权码、刷新令牌和客户端凭证三种授权模式。其中,授权码模式支持 PKCE 扩展,并强制要求使用 S256 算法来生成 Code Challenge 。管理员在创建 OAuth 客户端时,可以配置回调地址,并指定该客户端允许使用的授权模式(Grant Type)和权限范围(Scope)。
默认作用域在 config/scopes.ts:
| Scope | 含义 |
|---|---|
user | 访问用户信息 |
basic | 示例基础访问 |
角色描述的是用户本身拥有的权限,而 Scope 描述的是某个访问令牌被允许执行的操作。即使当前用户是管理员,第三方应用获得的 Token 也可能只包含 basic Scope,因此只能访问该 basic Scope 允许的 API。

与 Google 登录有什么不同
| 授权服务器 | 社交登录 | |
|---|---|---|
| 目的 | 第三方应用访问你的 API | 用户登录你的网站 |
| 页面 | /oauth/authorize* | /auth/login 等 |
| 回调 / 换取令牌 | /oauth/token | /api/auth/oauth2/google、/github |
| 配置 | deploy.oauth2、scopes.ts | GOOGLE_* / GITHUB_* 环境变量 |
/api/auth/current-user 读的是网站 Session。/oauth/current-user 读的是第三方应用的访问令牌。不要混用。
先体验授权流程
如果你的产品暂时不开放给第三方,仍建议先在后台看一眼客户端管理页,避免上线后才发现默认开关是打开的。

完整走通一次授权码流程:
管理员创建客户端,填写 Redirect URI 和允许的 scope
↓
第三方应用打开 /oauth/authorize
↓
用户登录(如尚未登录)
↓
在同意页确认权限
↓
带回授权码
↓
POST /oauth/token 换取 access token 和 refresh token默认授权码有效期 15 分钟,访问令牌 1 天,刷新令牌 30 天。刷新令牌会轮换。
提示
oauth2.enableService 只决定是否开放授权相关页面,关闭后 /oauth/token 等 API 接口依然可用。如果你的产品暂不对第三方开放,建议不要创建任何客户端,也不要在业务 API 中依赖这些令牌。
配置 OAuth 2.0 授权服务器
你需要在 config/deploy.ts 文件的 oauth2 配置项和 config/scopes.ts 文件里进行相关授权服务器的配置。
是否开放 OAuth 2.0 服务
默认:
oauth2: {
enableService: true,
issuer: 'saavo',
requiresPKCE: true,
requiresS256: true,
}产品初始化后,建议尽早修改 issuer,它会写入访问令牌的声明中,用于标识令牌的签发方。
如果产品不需要提供开放平台能力,可以将 enableService 设置为 false,并且不创建 OAuth 客户端。需要注意,当前这个开关只控制相关页面是否启用,并不会停止 OAuth API 本身。
如果希望彻底关闭授权服务器,还需要同时停止挂载 /oauth 相关 API 路由。
定义第三方可申请的授权范围
在 config/scopes.ts 增加自己的 scope,并写好名称和说明。这些文案会显示在同意页。
第三方实际能拿到的权限,是请求 scope 与客户端允许 scope 的交集。只在配置里新增 scope 还不够,还要在后台把该 scope 授给对应客户端。
配置授权范围升级提示
config/upgrade.ts 默认是空对象。当用户缺少某个权益时,可以在这里配置升级卡片,让同意页引导购买更高套餐。
没有升级需求就保持为空。不要把计费逻辑写进授权码换取令牌接口。
准备授权服务
授权码、访问令牌和刷新令牌的签名都依赖 SAAS_SECRET 环境变量。不同环境应该使用独立的密钥,尤其不要将开发或测试环境中的 SAAS_SECRET 直接用于生产环境。
第三方客户端使用的 Redirect URI 必须与管理后台中登记的地址完全一致。由于本地开发和生产环境通常使用不同域名,可以分别创建对应环境的 OAuth 客户端,也可以为同一个客户端配置多组允许的回调地址。
在业务 API 中验证访问令牌
只有当你主动把 API 开放给第三方时,才需要在业务路由里检查 OAuth token。
保护资源的基本原则:
先检查 token 的 scope,再按需检查该用户是否仍具备某个角色或权益。不要把用户的 admin 角色写进 token。
const result = await authz.authorizeOAuthRequest(c, {
scope: 'basic',
});
if (!result.success) {
return c.json(result, 401);
}如果这个接口还要求用户买过某个产品:
const result = await authz.authorizeOAuthRequest(c, {
scope: 'user',
entitlement: 'starter_download',
});也可以单独使用:
await authz.hasScopes(c, 'user')
await authz.hasAnyScope(c, ['user', 'basic'])第三方应用通过 OAuth2 访问资源时,应只暴露与当前 Scope 相匹配的数据。模板提供的 GET /oauth/current-user 只是一个示例,即使通过 user Scope 后会返回部分用户权益信息,也不代表自己的资源接口应该照搬全部字段;正式开放 API 时,应根据实际业务决定哪些用户信息、角色或权益可以提供给第三方。
OAuth2 主要用于第三方客户端访问 API,网站自身的页面和第一方 API 仍然继续使用 Session 和 authenticatedGuard,不需要改成 Bearer Token 认证。两套认证方式解决的是不同场景,不建议混用。
另外,client_credentials 获取的令牌不代表任何具体用户,因此也不能依赖用户的角色、权益或其他个人状态。它更适合服务与服务之间的机器访问,这类接口应仅根据 Token 的 Scope 决定是否允许访问。
提示
在 SaaS 业务中,只推荐使用两种授权模式,授权码模式和客户端凭证模式。其它授权模式在安全性上存在明显缺陷,不推荐使用。
上线检查
授权服务器会把用户权限交给第三方,上线前建议至少确认:
-
issuer已改成自己的产品标识。 - 生产环境使用独立的
SAAS_SECRET。 - 如不开放平台,未创建正式客户端且已关闭授权页面;如要求 API 也不可访问,已停止挂载
/oauth路由。 - 如开放平台,已用授权码 + PKCE 走通登录、同意、换票、刷新和撤销。
- 客户端 Redirect URI 已指向生产回调,而不是本地地址。
- 业务 API 检查的是 scope,而不是用户是否为管理员。
- 令牌拿不到未申请的 scope。
- 作用域文案已经换成自己的产品语言。
建议用一个专门的测试客户端完成上述流程,不要拿网站管理员 Session 代替 token 测试。
常见问题
接下来
根据接下来要开发的功能,可以继续阅读:
- 想让用户用 Google 登录 → 接入 Google 登录
- 想限制用户能做什么 → 权限与权益
- 想在同意页推荐升级 → 计费与套餐
- 想管理 OAuth 客户端 → 管理后台
- 想了解认证如何确定当前用户 → 认证与账号
大多数产品如果只是自己的 SaaS,并不需要把授权服务器当成核心功能。
只有确实要做开放平台时,再基于 scope 保护第三方 API。