OAuth 客户端与第三方登录
oak-general-business 里的 OAuth 能力,其实同时做了两件不同的事:
- 让 Oak 应用去接入第三方 OAuth 提供商,用第三方账号登录;
- 让 Oak 应用自己成为一个 OAuth 服务端,对外发授权码和 token。
如果不先把这两条线拆开,看代码时会非常容易绕晕。
主要对象
这一章的对象可以分成两组。
作为 OAuth 客户端,接入第三方登录
这一组对象包括:
OauthProviderOauthStateOauthUser
其中:
OauthProvider保存第三方提供商配置;OauthState保存登录或绑定时的 state;OauthUser保存第三方账号和 Oak 用户之间的连接关系。
作为 OAuth 服务端,对外提供授权
这一组对象包括:
OauthApplicationOauthAuthorizationCodeOauthTokenOauthUserAuthorization
其中:
OauthApplication表示一个外部客户端;OauthAuthorizationCode是授权码;OauthToken是服务端签发给客户端的 token;OauthUserAuthorization则记录用户是否授权、是否撤销。
组件
这一章已经提供了基础的管理和登录组件:
src/components/oauthsrc/components/oauth/managementsrc/components/oauth/recordssrc/components/login/oauth
另外,components/user/login/index.ts 也会根据应用的 applicationPassport 动态展示 OAuth 登录选项。
login/oauth/authorize 适合放在哪里
这组组件更像“OAuth 服务端授权确认页”。bm-smart/src/pages/oauth/authorize/web.pc.tsx 和 TripSlayer/src/pages/frontend/login/oauth/authorize/web.pc.tsx 的包法都非常薄:
<Auth oakPath="#Authorize" />
也就是说:
- 当前项目如果要作为 OAuth 服务端对外授权,页面层只要把这个组件挂出来
- 真正的授权码校验、用户确认、授权流程,还是走公共包
oauth 组件常用参数
oak-general-business/src/components/oauth/index.ts 是 OAuth 回调页组件,最关键的两个参数是:
onRetryonSuccess
它会自己从 URL 里读取:
codestateerrorerror_description
然后调用 features.token.loginByOAuth(code, state)。所以项目层真正要做的是“成功后回哪里、失败后怎么重试”。
oauth/management 的定位
这个组件更偏系统后台管理,而不是登录页。当前源码里它是一个虚拟组件,核心参数很少:
systemIdsystemName
它更适合做:
- OAuth provider 管理页
- OAuth application 管理页
- 系统级第三方登录配置入口
也就是说,项目里如果要做“OAuth 能力管理台”,通常应该把它挂到系统配置或平台配置页里,而不是混在普通登录页组件里。
进一步看 oak-general-business/src/components/oauth/management/web.pc.tsx,它当前并不是一个空壳,而是已经内置了两个页签:
providersapplications
对应的就是:
oauth/management/oauthProvideroauth/management/oauthApps
这两个子组件都会强依赖 systemId,并且内部直接按 systemId 过滤:
oauthProvider管提供商配置,如授权地址、token 地址、scope、clientId、clientSecretoauthApps管外部客户端,如redirectUris、isConfidential、requirePKCE
所以项目层如果只是要搭系统级 OAuth 管理后台,最稳妥的做法通常就是直接包 oauth/management,而不是自己重新拆 provider 表和 application 表。
oauth/records 的真实职责
这一组组件在原文里还没展开,但其实很适合直接写出来。oak-general-business/src/components/oauth/records/index.ts 当前的真实行为包括:
- 实体是
oauthUserAuthorization - 自动过滤当前登录用户
userId - 自动过滤当前应用所属系统
application.systemId - 默认只看
usageState in ['granted', 'denied', 'revoked'] - 默认分页
pageSize = 5 - 提供
revoke(item),实际走的是oauthUserAuthorization的revokeaction
也就是说,它不是 OAuth provider 管理页,而是“当前用户已经授权过哪些外部应用”的记录页,更适合放在:
- 个人中心
- 安全中心
- 第三方授权管理页
从 web.pc.tsx 的实际渲染看,它还已经把这些细节整理成可直接展示的卡片:
- 应用 logo / 名称 / 描述
- 当前授权状态
- scope
- 最近使用时间
- 撤销时间
项目层通常只需要把这个组件挂出来,不需要再自己判断哪些记录能 revoke。
oauth 回调页的真实参数语义
oak-general-business/src/components/oauth/index.ts 这页虽然只暴露了两个参数:
onRetryonSuccess
但它内部已经把回调页真正该做的工作都接上了:
- 从 URL 自动读取
code、state - 识别
error、error_description - 调用
features.token.loginByOAuth(code, state) - 失败时只要求项目层决定“怎么重试”
- 成功时只要求项目层决定“回哪里”
因此项目层不应该再自己重复写一遍“从 querystring 取 code/state 再换 Oak token”的逻辑。
aspect / feature
OAuth 相关的核心 aspect 在 src/aspects/oauth.ts:
loginByOauthgetOAuthClientInfocreateOAuthStateauthorize
从前端看,并没有单独的 oauth feature,登录动作是通过:
features.token.loginByOAuth(...)
来触发的。
这再次体现了 oak-general-business 的分层方式:登录入口还是收敛到 token feature,OAuth 只是其中的一条登录链路。
endpoint
这一章还有一组非常关键的 HTTP endpoint,定义在 src/endpoints/oauth.ts:
oauth/access_tokenoauth/userinfooauth/tokenoauth/revoke
它们分别负责:
- 用授权码换 token;
- 获取用户信息;
- 刷新 token;
- 撤销 token。
如果你要让 Oak 应用真的作为 OAuth 服务端对外工作,这一组 endpoint 就是最核心的公开入口。
需要额外说明的是:源码里并没有单独的 oauth/authorize endpoint。授权确认这一步走的是 authorize aspect,再配合 src/components/login/oauth/authorize 这个公共授权页组件完成。
watcher 与后台规则
OAuth 还带了一条后台补偿逻辑:
src/watchers/oauth.ts会定期刷新即将过期但仍可用的oauthUsertoken。
同时,相关 trigger 也不少:
triggers/oauthProvider.ts会根据 provider 变化维护passport(type='oauth');triggers/oauthUser.ts负责第三方登录后的一些用户侧补充逻辑;triggers/oauthUserAuth.ts负责授权撤销等行为的联动。
所以 OAuth 并不是只靠几个 endpoint 在工作,后台状态维护同样已经接好了。
注入点
OAuth 能力的注入点分成三层:
- 后端通过
ogb0Aspects、ogb0Endpoints、ogb0Watchers、ogb0Triggers注入; - 前端通过
components/oauth/*和features.token.loginByOAuth(...)进入; - 系统层还需要
Passport(type='oauth')和ApplicationPassport把这条登录方式真正暴露给某个应用。
如果少了最后一步,即使 OAuth provider 配好了,前端也不会真正展示对应的登录入口。
项目中如何接入
OAuth 在项目里一般分成两种接法:
- 把 Oak 当作客户端,去接第三方登录
- 把 Oak 当作服务端,对外暴露 token/userinfo/revoke endpoint,并通过授权页组件调用
authorizeaspect 完成授权确认
无论哪一种,最重要的都是:
- 应用和系统初始化时先把
ogb0Aspects/ogb0Endpoints合并进去; - 前端页面统一走
features.token.loginByOAuth(...)或公共登录组件; - 服务端统一复用
src/endpoints/oauth.ts,不要自己再造一套 OAuth 协议实现。
真实项目里的页面拆法
从 haina-busi 和 taicang 的现有代码来看,一个项目里最常见的是三类页面同时存在:
- 普通登录页里的第三方登录按钮,最终走
features.token.loginByOAuth /oauth/authorize这种授权确认页,直接包login/oauth/authorize/oauth这种回调页,直接包components/oauth
这三类页面分开之后,客户端登录和服务端授权就不会互相搅在一起。
taicang 当前的回调页和授权页包法都很薄,基本就是:
<OAuth oakPath="#OAuth" />
<Auth oakPath="#Authorize" />
这点很值得直接模仿。页面层真正需要做的通常只是:
- 给它稳定的
oakPath - 在外面包路由壳
- 如果未登录要先跳登录,再通过
onUnLogin把 OAuth 参数带回授权页
使用示例
1. 前端发起第三方 OAuth 登录
bm-smart/src/components/login/byOauth/ProviderList/index.ts 的真实做法是:
const state = await this.features.aspect.createOAuthState({
providerId: provider.id!,
type: 'login',
userId: this.features.token.getUserId(true),
});
window.location.href =
`${provider.authorizationEndpoint}?response_type=code` +
`&client_id=${clientId}` +
`&redirect_uri=${encodeURIComponent(redirectUri!)}` +
`&state=${state}`;
2. OAuth 回调页换回 Oak token
回调页最终只需要调用:
await this.features.token.loginByOAuth(code, state);
公共包里的 components/oauth、components/login/oauth/authorize 就是按这条链路工作的。
3. 作为 OAuth 服务端时复用 endpoint 与授权页组件
src/endpoints/oauth.ts 已经提供了:
oauth/access_tokenoauth/tokenoauth/revokeoauth/userinfo
而授权确认页则直接复用 src/components/login/oauth/authorize,内部会调用:
await this.features.cache.exec('authorize', {
response_type,
client_id,
redirect_uri,
scope,
state,
action: 'grant',
});
如果项目只是要把自己的用户体系对外开放,最推荐直接挂这组 endpoint,并复用公共授权页组件,而不是项目层自己重新实现授权码、refresh token 和 PKCE。
使用建议
这一章最重要的一条经验是:
先分清楚“第三方登录到我的 Oak 应用”与“我的 Oak 应用对外发 token”是两套对象。
理解了这一点,再去读 OauthProvider/OauthUser/OauthState 和 OauthApplication/OauthAuthorizationCode/OauthToken/OauthUserAuthorization,就不会再觉得它们重复了。