Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

OAuth 客户端与第三方登录

oak-general-business 里的 OAuth 能力,其实同时做了两件不同的事:

  • 让 Oak 应用去接入第三方 OAuth 提供商,用第三方账号登录;
  • 让 Oak 应用自己成为一个 OAuth 服务端,对外发授权码和 token。

如果不先把这两条线拆开,看代码时会非常容易绕晕。

主要对象

这一章的对象可以分成两组。

作为 OAuth 客户端,接入第三方登录

这一组对象包括:

  • OauthProvider
  • OauthState
  • OauthUser

其中:

  • OauthProvider 保存第三方提供商配置;
  • OauthState 保存登录或绑定时的 state;
  • OauthUser 保存第三方账号和 Oak 用户之间的连接关系。

作为 OAuth 服务端,对外提供授权

这一组对象包括:

  • OauthApplication
  • OauthAuthorizationCode
  • OauthToken
  • OauthUserAuthorization

其中:

  • OauthApplication 表示一个外部客户端;
  • OauthAuthorizationCode 是授权码;
  • OauthToken 是服务端签发给客户端的 token;
  • OauthUserAuthorization 则记录用户是否授权、是否撤销。

组件

这一章已经提供了基础的管理和登录组件:

  • src/components/oauth
  • src/components/oauth/management
  • src/components/oauth/records
  • src/components/login/oauth

另外,components/user/login/index.ts 也会根据应用的 applicationPassport 动态展示 OAuth 登录选项。

login/oauth/authorize 适合放在哪里

这组组件更像“OAuth 服务端授权确认页”。bm-smart/src/pages/oauth/authorize/web.pc.tsxTripSlayer/src/pages/frontend/login/oauth/authorize/web.pc.tsx 的包法都非常薄:

<Auth oakPath="#Authorize" />

也就是说:

  • 当前项目如果要作为 OAuth 服务端对外授权,页面层只要把这个组件挂出来
  • 真正的授权码校验、用户确认、授权流程,还是走公共包

oauth 组件常用参数

oak-general-business/src/components/oauth/index.ts 是 OAuth 回调页组件,最关键的两个参数是:

  • onRetry
  • onSuccess

它会自己从 URL 里读取:

  • code
  • state
  • error
  • error_description

然后调用 features.token.loginByOAuth(code, state)。所以项目层真正要做的是“成功后回哪里、失败后怎么重试”。

oauth/management 的定位

这个组件更偏系统后台管理,而不是登录页。当前源码里它是一个虚拟组件,核心参数很少:

  • systemId
  • systemName

它更适合做:

  • OAuth provider 管理页
  • OAuth application 管理页
  • 系统级第三方登录配置入口

也就是说,项目里如果要做“OAuth 能力管理台”,通常应该把它挂到系统配置或平台配置页里,而不是混在普通登录页组件里。

进一步看 oak-general-business/src/components/oauth/management/web.pc.tsx,它当前并不是一个空壳,而是已经内置了两个页签:

  • providers
  • applications

对应的就是:

  • oauth/management/oauthProvider
  • oauth/management/oauthApps

这两个子组件都会强依赖 systemId,并且内部直接按 systemId 过滤:

  • oauthProvider 管提供商配置,如授权地址、token 地址、scope、clientId、clientSecret
  • oauthApps 管外部客户端,如 redirectUrisisConfidentialrequirePKCE

所以项目层如果只是要搭系统级 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),实际走的是 oauthUserAuthorizationrevoke action

也就是说,它不是 OAuth provider 管理页,而是“当前用户已经授权过哪些外部应用”的记录页,更适合放在:

  • 个人中心
  • 安全中心
  • 第三方授权管理页

web.pc.tsx 的实际渲染看,它还已经把这些细节整理成可直接展示的卡片:

  • 应用 logo / 名称 / 描述
  • 当前授权状态
  • scope
  • 最近使用时间
  • 撤销时间

项目层通常只需要把这个组件挂出来,不需要再自己判断哪些记录能 revoke。

oauth 回调页的真实参数语义

oak-general-business/src/components/oauth/index.ts 这页虽然只暴露了两个参数:

  • onRetry
  • onSuccess

但它内部已经把回调页真正该做的工作都接上了:

  • 从 URL 自动读取 codestate
  • 识别 errorerror_description
  • 调用 features.token.loginByOAuth(code, state)
  • 失败时只要求项目层决定“怎么重试”
  • 成功时只要求项目层决定“回哪里”

因此项目层不应该再自己重复写一遍“从 querystring 取 code/state 再换 Oak token”的逻辑。

aspect / feature

OAuth 相关的核心 aspect 在 src/aspects/oauth.ts

  • loginByOauth
  • getOAuthClientInfo
  • createOAuthState
  • authorize

从前端看,并没有单独的 oauth feature,登录动作是通过:

  • features.token.loginByOAuth(...)

来触发的。

这再次体现了 oak-general-business 的分层方式:登录入口还是收敛到 token feature,OAuth 只是其中的一条登录链路。

endpoint

这一章还有一组非常关键的 HTTP endpoint,定义在 src/endpoints/oauth.ts

  • oauth/access_token
  • oauth/userinfo
  • oauth/token
  • oauth/revoke

它们分别负责:

  • 用授权码换 token;
  • 获取用户信息;
  • 刷新 token;
  • 撤销 token。

如果你要让 Oak 应用真的作为 OAuth 服务端对外工作,这一组 endpoint 就是最核心的公开入口。

需要额外说明的是:源码里并没有单独的 oauth/authorize endpoint。授权确认这一步走的是 authorize aspect,再配合 src/components/login/oauth/authorize 这个公共授权页组件完成。

watcher 与后台规则

OAuth 还带了一条后台补偿逻辑:

  • src/watchers/oauth.ts 会定期刷新即将过期但仍可用的 oauthUser token。

同时,相关 trigger 也不少:

  • triggers/oauthProvider.ts 会根据 provider 变化维护 passport(type='oauth')
  • triggers/oauthUser.ts 负责第三方登录后的一些用户侧补充逻辑;
  • triggers/oauthUserAuth.ts 负责授权撤销等行为的联动。

所以 OAuth 并不是只靠几个 endpoint 在工作,后台状态维护同样已经接好了。

注入点

OAuth 能力的注入点分成三层:

  • 后端通过 ogb0Aspectsogb0Endpointsogb0Watchersogb0Triggers 注入;
  • 前端通过 components/oauth/*features.token.loginByOAuth(...) 进入;
  • 系统层还需要 Passport(type='oauth')ApplicationPassport 把这条登录方式真正暴露给某个应用。

如果少了最后一步,即使 OAuth provider 配好了,前端也不会真正展示对应的登录入口。

项目中如何接入

OAuth 在项目里一般分成两种接法:

  • 把 Oak 当作客户端,去接第三方登录
  • 把 Oak 当作服务端,对外暴露 token/userinfo/revoke endpoint,并通过授权页组件调用 authorize aspect 完成授权确认

无论哪一种,最重要的都是:

  • 应用和系统初始化时先把 ogb0Aspects / ogb0Endpoints 合并进去;
  • 前端页面统一走 features.token.loginByOAuth(...) 或公共登录组件;
  • 服务端统一复用 src/endpoints/oauth.ts,不要自己再造一套 OAuth 协议实现。

真实项目里的页面拆法

haina-busitaicang 的现有代码来看,一个项目里最常见的是三类页面同时存在:

  • 普通登录页里的第三方登录按钮,最终走 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/oauthcomponents/login/oauth/authorize 就是按这条链路工作的。

3. 作为 OAuth 服务端时复用 endpoint 与授权页组件

src/endpoints/oauth.ts 已经提供了:

  • oauth/access_token
  • oauth/token
  • oauth/revoke
  • oauth/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/OauthStateOauthApplication/OauthAuthorizationCode/OauthToken/OauthUserAuthorization,就不会再觉得它们重复了。