Token 与多端登录态
如果说前一章解决的是“用户是谁”,那么这一章解决的就是“用户现在是以什么身份、从什么端、在什么应用里登录进来的”。
Oak 里的 Token 不是一个简单的 session 字符串。它还绑定了:
- 当前应用;
- 当前用户;
- 当前扮演者
player; - 当前环境
env; - token 的刷新时间和旧值。
这也是为什么 oak-general-business 可以同时处理 web、小程序、公众号、原生 App 这些不同环境的登录态。
主要对象
这一章的核心实体是 Token。
它保存了:
entity/entityId:token 关联的是谁;user/player:当前真实用户与当前扮演者;application:当前应用;env:登录环境;value/oldValue:当前 token 值与上一个 token 值;refreshedAt/disablesAt:刷新时间与禁用时间。
这里还有一个非常关键的设计:oldValue 允许刷新 token 后的一小段时间里仍能识别旧 token,这对前后端切换 token 值很有帮助。
组件
和登录态直接相关的组件主要有:
src/components/user/loginsrc/components/user/login/passwordsrc/components/user/login/smssrc/components/user/login/emailsrc/components/token/mesrc/components/common/weChatLoginGrantsrc/components/common/weChatLoginQrCode
其中 components/user/login/index.ts 是最值得读的一个例子。它会根据当前应用的 applicationPassport 动态决定:
- 展示哪些登录方式;
- 是否允许密码登录;
- 是否展示 OAuth 登录;
- 是否允许注册。
user/login 常用参数
oak-general-business/src/components/user/login/index.ts 暴露的参数比表面看起来多,但项目里最常用的是下面这些:
onlyCaptcha:只保留手机号验证码登录onlyPassword:只保留密码登录disabled:禁用某类登录方式redirectUri:微信登录成功后回跳到哪个wechatUser/login页面url:登录完成后最终要回到的业务页面callback:非微信登录成功后的回调goRegister:跳注册页isRegisterBack:从注册页返回时优先切到密码登录goOauthLogin:跳指定 OAuth 提供方登录
taicang/src/pages/frontend/login/web.tsx 的真实写法就是:
<Login
redirectUri={redirectUri}
url={backUrl}
callback={() => {
go();
}}
/>
user/login 真正是怎么决定显示哪些入口的
这一点值得直接写清楚,因为它不是一个静态登录页。
从 oak-general-business/src/components/user/login/index.ts 看,它在 ready() 里会先做这几步:
- 调
getApplicationPassports(applicationId)取当前应用真正启用的登录方式 - 找出
isDefault=true的默认登录方式 - 再结合本地存储里的
loginMode决定当前默认显示哪个 tab - 根据
sms/email/loginName上的allowPwd推导是否允许显示密码登录 - 根据
oauth类型passport.config.oauthIds去加载第三方 OAuth 提供方列表 - 从
passport.config.digit里取短信、邮箱验证码位数 - 从
application.system.config.Password里取密码存储模式和密码规则
也就是说,user/login 的显示来源同时依赖:
ApplicationPassportPassport.configSystem.config.Password- 本地存储里的上次登录方式
项目层如果发现登录页和后台配置不一致,优先应该回查这四层,而不是先改组件渲染。
user/login/password 常用参数
如果项目只想单独复用密码登录子组件,而不是整套 user/login,最值得先记住的是这些参数:
pwdAllowMobilepwdAllowEmailpwdAllowLoginNameallowSmsallowEmailallowWechatMpsetLoginModepwdModeallowRegistergoRegister
它当前的真实行为包括:
- 账号输入框占位文案会按
pwdAllowMobile / pwdAllowEmail / pwdAllowLoginName自动拼成“账号/手机号/邮箱”提示 - 提交前只校验“账号非空 + 密码满足
isPassword(...)” pwdMode === 'sha1'时,会先走encryptPasswordSha1(...)再调用features.token.loginByAccount(...)- 登录成功后优先走
callback,没有callback才按url跳转
所以它更适合:
- 项目已经确定只做密码登录
- 但仍然想保留“手机号/邮箱/账号名都可作为账号输入”的灵活性
user/login/sms 常用参数
短信登录子组件最关键的参数则是:
digitallowPasswordallowEmailallowWechatMpsetLoginModecallbackurl
它的真实行为也很值得写进文档:
- 发验证码固定走
features.token.sendCaptcha('mobile', mobile, 'login') - 登录固定走
features.token.loginByMobile(mobile, captcha) - 验证码格式校验直接使用
isCaptcha(value, digit) - 发送冷却时间会写本地存储,开发环境默认 10 秒,生产环境默认 60 秒
这意味着项目层如果只是想裁出“纯短信登录页”,直接用这个子组件会比从 user/login 再做条件裁剪更直接。
token/me 常用参数
oak-general-business/src/components/token/me/index.ts 更像“当前登录用户入口卡片”,项目里最常传:
loginUrl:未登录时跳去哪里myInfoUrl:查看我的资料manageUserUrl:进入用户管理onMyInfoClicked:自定义点击“我的资料”行为Body:在默认卡片下方追加项目自己的内容
taicang/src/pages/frontend/my/web.pc.tsx 里是这样接的:
<GeneralMe
oakPath="$$general-my"
loginUrl="/login"
myInfoUrl="/my/info"
manageUserUrl="/user/manage"
Body={<Button onClick={() => logout()}>{t('logout')}</Button>}
/>
token/me 的真实判断逻辑
这个组件看起来像一张简单的“我的”卡片,但它背后有几层很明确的 token 语义:
- 它查询的不是“全部 token”,而是按
features.token.getTokenValue()过滤当前本地 token 值 - 会额外再查一次
extraFile(tag1='avatar')来拼头像 URL isPlayingAnother的判断条件是token.userId !== token.playerIdisRoot取的是当前player.isRoot,不是单纯看user.isRoot
它的登录入口逻辑也不是固定跳转:
- 小程序环境
doLogin()会直接调用features.token.loginWechatMp() - Web 环境才会按
loginUrl跳独立登录页
所以 token/me 很适合做:
- 小程序首页“我的”卡片
- PC 前台右上角当前用户入口
- 需要区分“当前是不是在代入别的 player” 的后台入口
common/weChatLoginGrant / common/weChatLoginQrCode
这两个组件虽然不直接挂在 Token 实体上,但它们本质上都是给微信网页登录链路做“前置入口”。
weChatLoginGrant 适合做按钮式授权入口,常用参数包括:
appIdscoperedirectUristatedisableddisableTextdev
它的真实行为是:
- 生产环境直接跳微信 OAuth 授权地址
- 开发环境用本地模拟
code的方式跳到redirectUri disabled时不会跳转,而是提示disableText
weChatLoginQrCode 则适合桌面端扫码登录,常用参数也很接近:
appIdscoperedirectUristatedisableddisableTextdevhref
它的额外特点是:
- 生产环境会动态加载微信官方
wxLogin.js href可以覆盖默认二维码样式disabled时会显示一层“禁用微信二维码”的遮罩,而不是直接卸载组件
所以项目里如果需要“按钮授权”和“扫码授权”两种入口,并不需要自己拼 OAuth URL,直接复用这两个公共组件更稳。
前端 feature 与 aspect
这一章最重要的前端 feature 是 features.token。它几乎承载了整套登录行为:
loginByAccountloginByMobileloginByEmailbindByMobilebindByEmailsendCaptchaloginWechatloginWechatMploginWechatNativeloginByOAuthloginWebByMpTokenrefreshTokenlogoutswitchToverifyPasswordgetWechatMpUserPhoneNumberwakeupParasiterefreshWechatPublicUserInfosyncUserInfoWechatMp
对应的后端 aspect 集中在 src/aspects/token.ts,其中还包含:
sendCaptchaByMobilesendCaptchaByEmailbindByMobilebindByEmailrefreshWechatPublicUserInfosetUserAvatarFromWechat
这就是 oak-general-business 最典型的一种设计:登录态通过一个统一的 feature 暴露,后端则用一组命名清晰的 aspect 支撑它。
watcher 与后台规则
这一章还有一个必须知道的后台补偿逻辑:
src/watchers/token.ts会定期把已经到达disablesAt的 token 执行disable。
另外,refreshToken(...) 这条 aspect 还做了很多运行时保护:
- 检查 token 对应的环境和当前环境是否一致;
- 在 server 模式下按系统配置或默认间隔轮换 token 值;
- 必要时回写
applicationId。
所以 token 的刷新不是一个“前端本地行为”,而是 Oak 运行时和后台共同维护的一条业务链。
注入点
features.token 的注入点在 oak-general-business/src/features/index.ts:
create(...)时创建 token feature;- token feature 会订阅
applicationfeature,在应用识别成功后读取本地存储里的 token; initialize(...)在小程序环境下还会按需自动执行loginWechatMp()。
这意味着项目只要正确接入了 oak-general-business,小程序登录、token 本地缓存、刷新与失效,都会自动串起来。
项目中如何接入
Token 能力的项目接入非常固定:
createOgb0Features(...)注入features.tokeninitializeOgb0Features(...)里自动识别应用、读取本地 token、必要时执行小程序自动登录- 页面、组件、页面守卫统一只从
features.token读写登录态
也就是说,项目一旦把 oak-general-business 初始化链路接好,后面大部分页面都只需要关心“有没有登录”“当前用户是谁”。
真实项目里的常见页面落点
从 taicang 的现有页面看,token 相关组件大致会落在三个位置:
- 独立登录页:直接包
user/login - “我的”首页:已登录显示
token/me,未登录显示user/login - 需要先登录再继续的业务页:直接内嵌
user/login,并把redirectUri指向统一的/wechatUser/login
如果页面是桌面端工作台、运营后台或 PC 登录页,还很适合补:
- 一键授权按钮:
common/weChatLoginGrant - 扫码登录区:
common/weChatLoginQrCode
这类页面最重要的不是自己判断有哪些登录方式,而是保证当前应用的 ApplicationPassport 已经配置正确,让 user/login 自己读配置渲染。
使用示例
1. 账号密码登录
await this.features.token.loginByAccount(account, password);
const userId = this.features.token.getUserId(true);
2. 短信验证码登录
await this.features.token.sendCaptcha('mobile', mobile, 'login');
await this.features.token.loginByMobile(mobile, captcha);
这也是 src/components/mobile/login、src/components/user/login/sms 的真实调用方式。
3. 页面里判断登录态和退出登录
bm-smart 很多页面就是这么写的:
const loggedIn = !!this.features.token.getTokenValue();
const user = this.features.token.getUserInfo();
if (loggedIn) {
await this.features.token.logout();
}
4. 小程序场景直接拿手机号
await this.features.token.getWechatMpUserPhoneNumber(code);
如果你已经正确执行了 initializeOgb0Features(...),小程序端首次进入时还会按需自动触发 loginWechatMp()。
使用建议
对于组件开发来说,一条经验非常重要:
不要在页面里直接去拼
token查询和写入逻辑,而是统一走features.token。
因为登录、刷新、环境校验、自动登出、密码强度检查这些逻辑,已经被集中封装在这里了。绕开它,反而更容易把系统行为写乱。