From afd7e0dfa8a3d40d3ede3261f4f251656df1b224 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Thu, 14 May 2026 17:58:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20admin=20refresh=20token=20?= =?UTF-8?q?=E5=8F=8C=E4=BB=A4=E7=89=8C=E8=BD=AE=E8=BD=AC=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=20(PR=20#2285=20+=20#2287)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 测试服 9 步 curl 真测 PASS,通知前端 mmg 接 axios 401 拦截器自动续期。 Closes 通知 #2283 --- .../14_feat_admin_auth_refresh_token.md | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 changelogs/2026-05/14_feat_admin_auth_refresh_token.md diff --git a/changelogs/2026-05/14_feat_admin_auth_refresh_token.md b/changelogs/2026-05/14_feat_admin_auth_refresh_token.md new file mode 100644 index 0000000..7f8412f --- /dev/null +++ b/changelogs/2026-05/14_feat_admin_auth_refresh_token.md @@ -0,0 +1,185 @@ +# auth: admin 登录加 refresh token 双令牌轮转机制 + +> **服务**: hl-user-service (端口 8081) + hl-gateway (端口 8080) +> **PR**: #2285 + hotfix #2287 +> **Issue**: #2283 +> **日期**: 2026-05-14 +> **影响范围**: 管理后台所有调用方(hl-ui)的登录态管理 + +--- + +## ⚠️ 关键变化 + +**之前**: admin access token 固定 7 天过期,无刷新机制,过期 → 强制重登。 + +**现在**: +- **access token TTL 改为 2 小时**(可被环境变量 `ADMIN_ACCESS_TTL` 覆盖) +- 新增 **refresh token TTL 7 天** +- 新增 `POST /admin/auth/refresh` 用 refresh 换新 access(refresh 一次性轮转防重放) +- 登出时后端自动清两套 token + +**前端必做**:接 axios 401 拦截器自动调 `/admin/auth/refresh` 续期,否则用户 2h 后被强制登出。 + +--- + +## 接口变化 + +### 1. 登录响应新增 3 字段 + +`POST /admin/auth/login`、`POST /admin/auth/wechat/verify-2fa`、`POST /admin/auth/switch-role`、`GET /admin/auth/wechat-qr/status`、`GET /admin/auth/2fa/check` 所有签发 token 的接口,响应 `data` 多 3 字段: + +```json +{ + "code": 200, + "data": { + "token": "eyJhbGciOiJIUzI1NiJ9...", // 旧字段,=accessToken,保留向后兼容 + "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", // 新增,用于换新 access + "accessTokenExpiresIn": 7200, // 新增,access token 有效期(秒) + "refreshTokenExpiresIn": 604800, // 新增,refresh token 有效期(秒) + "adminId": 1002, + "username": "test_admin", + "role": "ADMIN", + "roleName": "管理员", + "avatar": null, + "roles": [...] + } +} +``` + +### 2. 新端点 `POST /admin/auth/refresh` + +| 项 | 值 | +|---|---| +| 方法/路径 | `POST /admin/auth/refresh` | +| Authorization | **不需要**(access 可能已过期) | +| body | `{ "refreshToken": "<当前 refresh token>" }` | +| 成功返回 | `Result` (与登录响应结构一致) | +| 失败返回 | `{ "code": 401, "message": "刷新令牌无效或已过期,请重新登录" }` | + +**轮转特性**:每次 refresh 成功,旧 refresh 立即失效(防重放)。前端必须立即用新返回的 `refreshToken` 替换本地存储的老值。 + +### 3. 登出行为变化 + +`POST /admin/auth/logout` 后端同时清: +- `admin_token:{adminId}` (access 白名单) +- `admin_refresh_token:{adminId}` (refresh 白名单) + +前端流程不变,登出后调 refresh 会立即报 401。 + +--- + +## 前端 axios 拦截器伪代码 (@mmg) + +```js +// 1. 登录后存全套 token +const { token, refreshToken } = res.data; +localStorage.setItem('admin_access', token); +localStorage.setItem('admin_refresh', refreshToken); + +// 2. 业务请求拦截:Authorization Bearer access +request.interceptors.request.use((cfg) => { + const access = localStorage.getItem('admin_access'); + if (access) cfg.headers.Authorization = `Bearer ${access}`; + return cfg; +}); + +// 3. 401 响应拦截:自动 refresh,失败才跳登录 +let refreshing = null; // 单飞防并发 401 一起触发多次 refresh +request.interceptors.response.use( + (res) => { + // 业务 code 401 也算未授权(本项目 HTTP 总是 200,401 在 body.code) + if (res.data?.code === 401) return handleAuth401(res); + return res; + }, + (err) => { + if (err.response?.status === 401) return handleAuth401(err); + return Promise.reject(err); + } +); + +async function handleAuth401(errOrRes) { + if (refreshing) { + await refreshing; + return request(errOrRes.config); // 等别人 refresh 完用新 token 重发 + } + refreshing = (async () => { + const refresh = localStorage.getItem('admin_refresh'); + if (!refresh) throw new Error('no refresh'); + try { + const r = await axios.post('/admin/auth/refresh', { refreshToken: refresh }); + if (r.data?.code !== 200) throw new Error('refresh failed'); + const { token, refreshToken } = r.data.data; + localStorage.setItem('admin_access', token); + localStorage.setItem('admin_refresh', refreshToken); + } catch (e) { + // refresh 也挂了 -> 清状态 -> 跳登录 + localStorage.removeItem('admin_access'); + localStorage.removeItem('admin_refresh'); + router.push('/login'); + throw e; + } finally { + refreshing = null; + } + })(); + await refreshing; + // 用新 access 重发原请求 + errOrRes.config.headers.Authorization = `Bearer ${localStorage.getItem('admin_access')}`; + return request(errOrRes.config); +} +``` + +**关键点**: +- 单飞锁 `refreshing` 防并发 401 一起触发多次 refresh(会被 jti 轮转拒) +- refresh 失败 → 清本地 → 跳 `/login`(等同于以前 401 弹窗的兜底) +- 老前端不发版也能用(老 access 7d→2h 后会被踢,但 `token` 字段还在,业务调用方式不变) + +--- + +## 测试服 9 步真测 PASS (admin / Admin@123456) + +``` +STEP 1: login -> 200 + 4 字段齐全 (accessTTL=7200, refreshTTL=604800) +STEP 2: GET /admin/auth/info Bearer access1 -> 200 (access 可用) +STEP 3: POST /admin/auth/refresh {refresh1} -> 200 + 新 access2+refresh2 +STEP 4: GET /admin/auth/info Bearer access2 -> 200 (新 access 可用) +STEP 5: POST /admin/auth/refresh {OLD refresh1} -> 401 (轮转防重放) +STEP 6: POST /admin/auth/logout Bearer access2 -> 200 +STEP 7: POST /admin/auth/refresh {refresh2 登出后} -> 401 (Redis 清掉) +STEP 8: POST /admin/auth/refresh {access1} -> 401 (access 当 refresh,type 校验) +STEP 9: POST /admin/auth/refresh {篡改 refresh} -> 401 +``` + +--- + +## 配置项 + +| 配置 key | 默认 | 环境变量 | +|---|---|---| +| `auth.jwt.admin-access-ttl-seconds` | 7200 (2h) | `ADMIN_ACCESS_TTL` | +| `auth.jwt.admin-refresh-ttl-seconds` | 604800 (7d) | `ADMIN_REFRESH_TOKEN_TTL` | + +代码层已 fallback,nacos 不配也跑;需要调整时通过环境变量或 nacos 配置覆盖。 + +--- + +## ⚠️ 正式服上线建议 + +正式服部署前,在 Deploy Panel 给 hl-user-service 设环境变量 `ADMIN_ACCESS_TTL=604800`(把 access 临时维持 7 天),等前端 mmg 接好 axios 拦截器后再去掉环境变量自动切回 2h。否则一上线所有 admin 用户 2h 后被强制踢下线。 + +测试服不需要(用户只有 wx/test_admin)。 + +--- + +## 不影响范围 + +- 小程序 user (USER 类型 token) — 现状 90 天不变 +- 登录端点 path 不变 +- /admin/auth/info 等业务接口契约不变 +- 已签发的 token 在 Redis 还有原 TTL,不会因部署立即失效 + +--- + +## 相关文档 + +- Issue: [wx/HL#2283](https://git.1814.love:8443/wx/HL/issues/2283) +- PR: [wx/HL#2285](https://git.1814.love:8443/wx/HL/pulls/2285) + hotfix [wx/HL#2287](https://git.1814.love:8443/wx/HL/pulls/2287) (补 user-service TokenInterceptor 白名单)