> # ⛔ 本篇已作废(结论有误,勿按此执行)
> 经浏览器实测复现 + 前后端源码/生产包核对,本篇两处关键结论都错了:
> ① 前端**早就接了 refresh 且生产已部署**(那次「零命中」是在落后 master 160 提交的过期本地代码上搜的);
> ② refresh **不轮转**(后端是滑动续期,串不变)。
> 真正根因是**前端路由守卫与 refresh 续期的竞态**,详见同目录
> **`26_notice_admin_relogin_REAL_root_cause_router_guard_refresh_race.md`**。
>
> ---

# ~~notice(后台登录): admin 登录态约 2 小时就过期 → 需前端接入 refresh 静默续期(根因排查结论)~~【作废,见上】

> **类型**: notice(后端无代码改动;双 token 机制后端早已就绪,缺前端接入)
> **关联 PR**: 无(后端不改);双 token 机制为工单 #2309 既有能力
> **日期**: 2026-05-26
> **影响接口**: `POST /admin/auth/login`(登录,已返回 refreshToken) + `POST /admin/auth/refresh`(刷新)
> **接收方**: mmg
> **前端**: **必须接入**(否则管理员每约 2 小时必被强制弹「登录已过期」一次)

---

## 🎯 背景 / 现象

运营反馈正式后台管理端「登录状态老短时间就过期」,弹窗提示「登录已过期 / invalid token」,重新登录后用一会儿又掉。

经后端彻底排查(正式环境实测),结论如下:

- admin 的 **access token 固定 2 小时过期**(绝对时间,不随操作滑动续期)。正式 nacos 未单独配置过期时长,走后端默认 7200 秒。
- 后端**双 token 机制完整且正常**:登录已返回 `refreshToken`(有效期 30 天),`/admin/auth/refresh` 接口实测可正常用 refreshToken 换发新 access。
- JWT 签名/验签、网关校验**均正常**,排除密钥/配置问题。
- **唯一缺口在前端**:hl-ui 全量检索 `refreshToken` / `auth/refresh` **零命中** —— 前端登录后**没有保存 refreshToken,也没有任何静默续期逻辑**。access token 一过 2 小时,前端收到 401 就直接弹「登录已过期」让用户重新登录。

即:双 token 续期闭环的「后端发券」已就绪,「前端用券续期」这一段没做,导致 access 一过期只能重新登录。

---

## 📋 后端接口契约(已就绪,可直接接)

### 1. 登录 `POST /admin/auth/login`(已存在,无变化)

响应 `data` 字段(`AdminLoginResponse`):

| 字段 | 类型 | 说明 |
|------|------|------|
| `token` | string | access token,有效期见 `accessTokenExpiresIn` |
| `refreshToken` | string | **刷新令牌,前端必须保存**(此前可能被前端忽略丢弃) |
| `accessTokenExpiresIn` | number | access 有效期**秒数**,当前 = `7200`(2 小时) |
| `refreshTokenExpiresIn` | number | refresh 有效期**秒数**,当前 = `2592000`(30 天) |
| `adminId` / `username` / `role` / `roleName` / `roles` / `avatar` / `mobile` 等 | - | 用户信息(不变) |

### 2. 刷新 `POST /admin/auth/refresh`(换发新 access)

- **无需 Authorization 头**(网关已放行该路径)
- 请求体:

```json
{ "refreshToken": "登录时保存的 refreshToken" }
```

- 响应 `data` 结构**与登录响应完全一致**(同样是 `AdminLoginResponse`),其中:
  - `token`:**新的 access token**(前端用它替换旧 access)
  - `refreshToken`:**新的 refresh token —— 会轮换!与旧的不同,前端必须一并更新保存**(实测每次 refresh 都会返回新串,旧串随后失效)
  - `accessTokenExpiresIn` / `refreshTokenExpiresIn`:同上

- 失败(refreshToken 过期 / 无效 / 已被新登录覆盖)时返回非 200 业务码,此时才真正需要跳登录页。

---

## ⚠️ 前端需要做的事

1. **保存 refreshToken**:登录成功后,把 `data.refreshToken` 与 access token 一起存起来(同 access 的存储介质)。
2. **静默续期**(二选一或叠加):
   - 方案 A(推荐,无感):根据 `accessTokenExpiresIn` 设定时器,在 access 临近过期(如剩余 5 分钟)时,后台静默调 `/admin/auth/refresh` 换新,**不打扰用户**。
   - 方案 B(兜底):在 axios 响应拦截器中,捕获 access 过期的 401(`Invalid token`),**先尝试用 refreshToken 调 `/admin/auth/refresh`**,成功则用新 access **重放原失败请求**;只有 refresh 也失败时,才走现有的「登录已过期」弹窗 + 跳登录页逻辑(`src/utils/request.js` 现有 401 分支)。
3. **每次 refresh 后更新存储**:用返回的新 `token` 和新 `refreshToken` 覆盖旧值(refreshToken 会轮换,务必更新,否则下次续期会拿到失效的旧串)。
4. **并发请求处理**:多个请求同时 401 时,只发起一次 refresh,其余请求挂起等新 token 后重放(避免重复刷新)。现有代码已有 `isHandling401` 单飞标志,可复用同样思路做 refresh 单飞。
5. **登出 / refresh 失败**:清空 access + refreshToken,跳登录页(沿用现有逻辑)。

---

## 🔧 后端状态

- 后端**无需任何改动**,`/admin/auth/refresh` 与登录返回 refreshToken 均已在正式环境就绪并实测通过。
- 如前端短期内无法接入,后端可作为**临时缓解**调大 access 有效期(改正式 nacos `admin-access-ttl-seconds` 并重启),但这会拉长 token 泄露窗口,**非长久之计**,正解仍是前端接入 refresh 续期。是否启用此临时方案由后端按需评估。

---

## ✅ 验证方式(前端接入后)

1. 登录后等 access 过期(可临时把判定阈值调短便于测试),期间持续操作,应**全程无「登录已过期」弹窗**,网络面板可见自动发起的 `/admin/auth/refresh`。
2. 手动篡改本地 access 为过期串发请求,应触发静默 refresh + 原请求重放成功。
3. 把本地 refreshToken 也置为无效,再触发,应正常落到「登录已过期」弹窗 + 跳登录页。
