切换日光/暗黑模式
018. Token 缓存与登录流程
学习目标
这一节开始处理前端登录态,重点是 token 缓存、token 获取服务和登录页跳转。
学完后,你应该能理解:
- 为什么受保护页面需要 token;
pages、private、public三类页面为什么要区分 token;- access token 和 refresh token 分别负责什么;
- token 缓存器要封装哪些能力;
- token 获取服务为什么会有多种分支;
- 登录成功后为什么要跳回原来的页面。
哪些页面需要 token
项目里会有不同类型的页面。
大致可以分成:
| 类型 | 特点 |
|---|---|
public | 不要求登录,例如登录页 |
pages | 普通业务页面,需要用户登录 |
private | 更偏管理或内部页面,也需要登录和权限 |
需要登录的页面,在请求后端接口时要带 token。
例如:
- 查询当前用户信息;
- 上传文件;
- 创建报销单;
- 查询受保护数据;
- 访问管理菜单。
后端通过 token 判断当前请求是谁发起的、是否已经登录、是否有权限访问。
为什么 token 要分命名空间
pages 和 private 可能使用不同登录态。
如果它们都把 token 存到同一个 key 里,就可能互相覆盖。
例如:
- 普通业务页面登录了用户 A;
- 管理后台登录了管理员 B;
- 两边都写入同一个
access_token; - 后登录的一边把前一边覆盖掉。
所以缓存 token 时要加前缀,例如:
txt
pages_access_token
private_access_token这类前缀就是 token 的命名空间。
token 缓存器
项目会封装 createTokenSaver 这类函数。
它负责和本地缓存打交道,而不是让业务代码到处直接读写 localStorage。
常见能力包括:
- 获取 access token;
- 保存 access token;
- 获取 refresh token;
- 保存 refresh token;
- 清理 token;
- 按页面类型加缓存前缀。
这样后续如果缓存策略要调整,只需要改 token 缓存器,不用全项目搜索 localStorage。
函数式封装而不是类
token 缓存器可以写成类,也可以写成函数返回对象。
课程里更偏向函数式封装。
例如传入缓存前缀后,返回一组方法:
ts
const tokenSaver = createTokenSaver('pages');
tokenSaver.getAccessToken();
tokenSaver.saveAccessToken(token, expiresAt);
tokenSaver.clear();这种写法和 React Hook、Vue Composition API 的风格更接近:用函数组合能力,而不是大量写类。
ReturnType 类型工具
TypeScript 里可以用 ReturnType 获取一个函数的返回值类型。
例如:
ts
type TokenSaver = ReturnType<typeof createTokenSaver>;这样不用手写一遍返回对象的类型。
如果 createTokenSaver 的返回结构变了,TokenSaver 也会跟着更新。
ReturnType 是 TypeScript 内置类型,不需要额外导入。
access token 和 refresh token
登录态通常不只保存一个 token。
| 名称 | 作用 |
|---|---|
| access token | 每次请求接口时携带,证明当前用户身份 |
| refresh token | access token 过期后,用来换新的 access token |
access token 使用频率很高,每个受保护接口都可能带上它,所以更容易暴露。
如果 access token 有效期设置成很多天,一旦泄露,风险会很大。
因此常见做法是:
- access token 有效期较短;
- refresh token 有效期较长;
- access token 过期后,用 refresh token 换新的 access token;
- refresh token 也过期时,要求用户重新登录。
获取 token 的四种情况
token 获取服务要处理几种状态。
| 情况 | 处理方式 |
|---|---|
| 没有 token | 跳转登录 |
| access token 未过期 | 直接返回 access token |
| access token 过期,refresh token 未过期 | 调刷新接口换新 token |
| 两个 token 都过期 | 清理缓存并跳转登录 |
这就是 createTokenService 的核心逻辑。
业务请求不应该自己判断这些细节,而是调用 token 服务拿可用 token。
为什么拿不到 token 要抛异常
如果当前请求必须登录,但 token 服务发现没有可用 token,就不能继续发送请求。
这时需要终止流程。
在代码里可以通过抛异常来中断后续逻辑,让请求封装知道:
- 当前用户需要登录;
- 不能继续请求受保护接口;
- 需要跳转登录页;
- 或者弹出对应提示。
如果不终止请求,后端大概率会返回 401,前端体验也会更混乱。
登录后跳回原页面
用户访问受保护页面时,如果没有登录,会跳到登录页。
但登录成功后,不应该总是回首页,而应该尽量回到刚才想访问的页面。
所以跳转登录页时,要把原页面信息带过去:
- 原路径;
- 路由参数;
- 查询参数;
- 当前 layout 类型。
登录成功后,登录页再根据这些信息跳回原页面。
例如用户原本打开的是某个报销单详情页,登录后应该回到这张报销单,而不是丢到默认首页。
登录页面和资源
登录页会参考 Ant Design Pro 的视觉结构。
当前只实现账号密码登录。
手机验证码登录暂时不做,因为短信服务通常需要企业资质,学习项目没必要一开始就引入这个成本。
登录页除了 TSX 代码,还会有样式、logo、背景图等静态资源。这些资源需要放到正确目录,否则页面结构能出来,但图片不会显示。
mock 登录接口
前端登录流程早期可以先用 mock 接口验证。
需要模拟的接口包括:
- 登录;
- 刷新 token;
- 获取当前用户信息。
这样即使后端接口还没完全接好,也可以先验证前端登录流程、token 保存和页面跳转是否正确。
mock 的重点不是假装功能完成,而是先把前端流程跑通,等真实后端接入后再替换。