fix: 主动token刷新-在过期前自动续期避免401错误 #1385

Closed
xiaoxia wants to merge 2 commits from fix/proactive-token-refresh into develop
Owner

问题

access_token 每 30 分钟过期一次。过期后第一个 API 请求会触发 401,然后拦截器再刷新 token 重试。虽然拦截器能自动处理,但:

  1. Network 面板会显示 401 错误
  2. 如果刷新也失败(refresh_token 过期),用户会被突然跳转到登录页
  3. 用户体验不好——看到 401 会以为系统出了问题

修复方案

主动 token 刷新:在 access_token 过期前 60 秒自动刷新,完全避免 401 出现。

新增文件

  • apps/web/src/api/auth/tokenRefresh.ts — 主动刷新模块
    • decodeJwtPayload():解码 JWT payload 获取 exp(无需第三方库)
    • scheduleProactiveRefresh():根据过期时间调度定时器,到期前自动刷新
    • cancelProactiveRefresh():取消已调度的定时器

修改文件

  • apps/web/src/main.tsx — 应用启动时,如果已登录则立即调度主动刷新
  • apps/web/src/hooks/useAuth.ts — 登录/微信回调成功后调度,登出时取消
  • apps/web/src/api/client.ts — 拦截器刷新成功后重新调度(基于新 token 过期时间),刷新失败时取消

工作流程

登录成功 → scheduleProactiveRefresh()
  ↓
解码 JWT 获取 exp → 计算 delay = exp - now - 60s
  ↓
setTimeout(delay) → 调用 refreshAccessToken()
  ↓
刷新成功 → 更新 token → scheduleProactiveRefresh()(递归)
刷新失败 → cancelProactiveRefresh() → clearAuth → 跳转 /login

验收

  • 登录 staging 后,等待超过 30 分钟(或手动修改 JWT exp 测试)
  • Network 面板不应出现 401 错误
  • Token 应在过期前 60 秒自动刷新
## 问题 access_token 每 30 分钟过期一次。过期后第一个 API 请求会触发 401,然后拦截器再刷新 token 重试。虽然拦截器能自动处理,但: 1. Network 面板会显示 401 错误 2. 如果刷新也失败(refresh_token 过期),用户会被突然跳转到登录页 3. 用户体验不好——看到 401 会以为系统出了问题 ## 修复方案 **主动 token 刷新**:在 access_token 过期前 60 秒自动刷新,完全避免 401 出现。 ### 新增文件 - `apps/web/src/api/auth/tokenRefresh.ts` — 主动刷新模块 - `decodeJwtPayload()`:解码 JWT payload 获取 exp(无需第三方库) - `scheduleProactiveRefresh()`:根据过期时间调度定时器,到期前自动刷新 - `cancelProactiveRefresh()`:取消已调度的定时器 ### 修改文件 - `apps/web/src/main.tsx` — 应用启动时,如果已登录则立即调度主动刷新 - `apps/web/src/hooks/useAuth.ts` — 登录/微信回调成功后调度,登出时取消 - `apps/web/src/api/client.ts` — 拦截器刷新成功后重新调度(基于新 token 过期时间),刷新失败时取消 ## 工作流程 ``` 登录成功 → scheduleProactiveRefresh() ↓ 解码 JWT 获取 exp → 计算 delay = exp - now - 60s ↓ setTimeout(delay) → 调用 refreshAccessToken() ↓ 刷新成功 → 更新 token → scheduleProactiveRefresh()(递归) 刷新失败 → cancelProactiveRefresh() → clearAuth → 跳转 /login ``` ## 验收 - 登录 staging 后,等待超过 30 分钟(或手动修改 JWT exp 测试) - Network 面板不应出现 401 错误 - Token 应在过期前 60 秒自动刷新
xiaoxia added the frontendP1 labels 2026-08-15 23:49:34 +08:00
xiaoxia added 1 commit 2026-08-17 14:19:17 +08:00
fix: 主动token刷新-在过期前自动续期避免401错误
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Failing after 0s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 0s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 36s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 0s
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 0s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 2m3s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m18s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m25s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m28s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m35s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 7s
8172eabda5
- 新增 tokenRefresh.ts:解码JWT获取过期时间,提前60秒自动刷新
- useAuth.ts:登录/微信回调成功后调度主动刷新,登出时取消
- client.ts:拦截器刷新成功后重新调度,刷新失败时取消
- main.tsx:应用启动时如果已登录则立即调度

修复:access_token每30分钟过期后第一个API请求触发401的问题
xiaoxia force-pushed fix/proactive-token-refresh from 2c60d97320 to 8172eabda5 2026-08-17 14:19:17 +08:00 Compare
xiaoxia added 1 commit 2026-08-17 14:49:00 +08:00
fix: 修复 useAuth 测试 mock 支持 getState() 方法
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 35s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 0s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m11s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m47s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m58s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m14s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m50s
AI Code Review / AI Code Review (pull_request) Failing after 4m20s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 6m17s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 1m13s
Preview Cleanup / Cleanup Preview Environment (pull_request) Failing after 0s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Failing after 1s
c305ae51ff

🚀 预览环境已部署

项目 详情
PR号 #1385
预览链接 https://pr-1385.preview.xiaoxiajianji.com
API环境 staging

💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。

🔄 每次提交新代码后预览环境会自动更新。

🗑️ PR 关闭或合并后,预览环境会自动清理。

🚀 **预览环境已部署** | 项目 | 详情 | |------|------| | PR号 | #1385 | | 预览链接 | [https://pr-1385.preview.xiaoxiajianji.com](https://pr-1385.preview.xiaoxiajianji.com) | | API环境 | staging | > 💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。 > > 🔄 每次提交新代码后预览环境会自动更新。 > > 🗑️ PR 关闭或合并后,预览环境会自动清理。
auto-approve-bot approved these changes 2026-08-17 14:51:51 +08:00
auto-approve-bot left a comment
Collaborator

CI全绿,自动审批通过。

CI全绿,自动审批通过。
auto-approve-bot approved these changes 2026-08-17 14:51:51 +08:00
auto-approve-bot left a comment
Collaborator

CI全绿,自动审批通过。

CI全绿,自动审批通过。
Collaborator

【阻塞级判定】

  • 是否存在阻塞级问题:是
  • 阻塞级问题数量:2 个

📊 审查概览

  • 整体评价:需修改
  • 建议级问题数量:1 个

🔴 阻塞级问题(必须修复)

  1. [apps/web/src/api/auth/tokenRefresh.ts: 63] 数据源不一致导致刷新失效

    • 问题类型:逻辑bug
    • 问题描述:scheduleProactiveRefresh 函数中,accessTokenlocalStorage 读取,而 refreshTokenuseAuthStore.getState() 读取。在 main.tsx 应用启动时调用此函数,若 Zustand 的 persist 中间件尚未完成水合(hydration),refreshToken 将为 undefined,导致函数提前返回,主动刷新失效。用户刷新页面后 Token 将无法自动续期。
    • 修改建议:为了保持一致性并规避 Store 水合时序问题,建议 refreshToken 也直接从 localStorage 读取,或者确保调用前 Store 已完全水合。推荐修改为:const refreshTokenValue = localStorage.getItem("refresh_token")
  2. [apps/web/src/api/auth/tokenRefresh.ts: 78] 非空断言存在运行时崩溃风险

    • 问题类型:逻辑bug
    • 问题描述:代码中使用 useAuthStore.getState().user! 强制断言 user 存在。如果在某些极端状态(如 Store 数据部分丢失、持久化配置未包含 user 字段)下 user 为空,此处将抛出 Uncaught Error,导致整个应用崩溃,而非降级处理为登出。
    • 修改建议:移除 !,增加显式检查。如果 user 不存在,应视为异常状态,执行清除认证逻辑或尝试重新获取用户信息,而不是直接崩溃。

💡 改进建议(不阻塞合并)

  1. [apps/web/src/api/auth/tokenRefresh.ts: 51] 并发刷新竞态条件风险
    • 具体内容:scheduleProactiveRefresh 中的 setTimeout 回调会直接调用 refreshAccessToken。如果此时恰好有一个网络请求返回 401 触发了 client.ts 中的拦截器刷新,两个刷新请求将并发执行。如果后端 Refresh Token 是一次性使用的,或者有严格的并发锁限制,其中一个请求会失败导致用户被强制登出。建议引入全局锁(如 isRefreshing 标志)或复用 refreshAccessToken 内部的 Promise 缓存机制来确保同一时刻只有一个刷新请求在执行。

良好实践

  • JWT 手动解码逻辑正确,处理了 Base64URL 填充问题。
  • useLogout 和刷新失败时正确调用了 cancelProactiveRefresh,防止了内存泄漏和无效的定时器触发。
  • 引入缓冲时间 REFRESH_BUFFER_SECONDS 是很好的防御性编程实践。

🤖 由 AI 代码审查机器人自动生成 | 2026-08-17 06:53:20 | 模型:

### 【阻塞级判定】 - 是否存在阻塞级问题:是 - 阻塞级问题数量:2 个 ### 📊 审查概览 - 整体评价:需修改 - 建议级问题数量:1 个 ### 🔴 阻塞级问题(必须修复) 1. **[apps/web/src/api/auth/tokenRefresh.ts: 63] 数据源不一致导致刷新失效** - 问题类型:逻辑bug - 问题描述:`scheduleProactiveRefresh` 函数中,`accessToken` 从 `localStorage` 读取,而 `refreshToken` 从 `useAuthStore.getState()` 读取。在 `main.tsx` 应用启动时调用此函数,若 Zustand 的 persist 中间件尚未完成水合(hydration),`refreshToken` 将为 `undefined`,导致函数提前返回,主动刷新失效。用户刷新页面后 Token 将无法自动续期。 - 修改建议:为了保持一致性并规避 Store 水合时序问题,建议 `refreshToken` 也直接从 `localStorage` 读取,或者确保调用前 Store 已完全水合。推荐修改为:`const refreshTokenValue = localStorage.getItem("refresh_token")`。 2. **[apps/web/src/api/auth/tokenRefresh.ts: 78] 非空断言存在运行时崩溃风险** - 问题类型:逻辑bug - 问题描述:代码中使用 `useAuthStore.getState().user!` 强制断言 `user` 存在。如果在某些极端状态(如 Store 数据部分丢失、持久化配置未包含 user 字段)下 `user` 为空,此处将抛出 Uncaught Error,导致整个应用崩溃,而非降级处理为登出。 - 修改建议:移除 `!`,增加显式检查。如果 `user` 不存在,应视为异常状态,执行清除认证逻辑或尝试重新获取用户信息,而不是直接崩溃。 ### 💡 改进建议(不阻塞合并) 1. **[apps/web/src/api/auth/tokenRefresh.ts: 51] 并发刷新竞态条件风险** - 具体内容:`scheduleProactiveRefresh` 中的 `setTimeout` 回调会直接调用 `refreshAccessToken`。如果此时恰好有一个网络请求返回 401 触发了 `client.ts` 中的拦截器刷新,两个刷新请求将并发执行。如果后端 Refresh Token 是一次性使用的,或者有严格的并发锁限制,其中一个请求会失败导致用户被强制登出。建议引入全局锁(如 `isRefreshing` 标志)或复用 `refreshAccessToken` 内部的 Promise 缓存机制来确保同一时刻只有一个刷新请求在执行。 ### ✅ 良好实践 - JWT 手动解码逻辑正确,处理了 Base64URL 填充问题。 - 在 `useLogout` 和刷新失败时正确调用了 `cancelProactiveRefresh`,防止了内存泄漏和无效的定时器触发。 - 引入缓冲时间 `REFRESH_BUFFER_SECONDS` 是很好的防御性编程实践。 --- <sub>🤖 由 AI 代码审查机器人自动生成 | 2026-08-17 06:53:20 | 模型: </sub> <!-- AI_CODE_REVIEW_AUTO_COMMENT -->
xiaoxia closed this pull request 2026-08-18 11:07:25 +08:00
Some checks are pending
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 35s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 0s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m11s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m47s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m58s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m14s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m50s
AI Code Review / AI Code Review (pull_request) Failing after 4m20s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 6m17s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 1m13s
Preview Cleanup / Cleanup Preview Environment (pull_request) Failing after 0s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Failing after 1s

Pull request closed

Sign in to join this conversation.