hl-api-changelog/changelogs/2026-05/15_feat_auth_refresh_token_sliding.md
2026-05-15 10:27:34 +08:00

10 KiB

auth: refreshToken 滑动续期 + 双 token 机制(admin + 小程序 user)

服务: hl-user-service (8081) + hl-mp-service (8085) + hl-gateway (8080) PR: #2312 (已合 dev,测试服待 Deploy Panel 部署) Issue: #2309 日期: 2026-05-15 影响范围: 管理后台 hl-ui + 微信小程序两端的登录态 / 401 拦截器


⚠️ 关键变化(对前一版 #2285 的演进)

维度 上一版 PR #2285 (2026-05-14) 本版 PR #2312 (2026-05-15)
适用端 仅 admin admin + 小程序 user 双端
accessToken TTL admin 2h admin 30 分钟 / user 30 分钟
refreshToken TTL admin 7d admin 7d / user 90d
续期模式 refresh 字符串轮转(每次换新串,旧立即失效) 滑动续期 — refresh 字符串不换,只刷 Redis 中 jti 的 TTL
灰度策略 直接切 网关新 key 优先 + 旧 key fallback(2026-05-22 删 fallback)
字段名 data.token = access (兼容) + refreshToken 同上,新增 data.accessToken 显式字段,data.token 仍保留

核心新原则:refreshToken 字符串后端不换,前端无需更新本地存的 refresh,只更新 access。这是与 #2285 最大的行为差异 —— 上一版前端必须每次 refresh 后立即覆盖本地 refresh,这一版不需要(覆盖也无害)。


一、背景

PR #2285 上线后用户中途反馈:rotation 模式遇到弱网/重复请求/双标签页时,新 refresh 还没收到旧 refresh 已被废,触发误踢登录。本版改为滑动续期:refresh 字符串保持稳定 7d/90d 内不变,只刷 Redis TTL,既解决误踢又保留"长期不活跃自动失效"语义。同时把小程序 user 端也接入(此前 user 端是无 refresh 机制 90 天硬到期)。

TTL 配置(线上确认值):

accessToken refreshToken nacos key
admin 30 分钟 (1800s) 7 天 (604800s) auth.jwt.admin-access-ttl-seconds / auth.jwt.admin-refresh-ttl-seconds
小程序 user 30 分钟 (1800s) 90 天 (7776000s) auth.jwt.user-access-ttl-seconds / auth.jwt.user-refresh-ttl-seconds

灰度期: 2026-05-15 ~ 2026-05-22,7 天后后端会清掉网关里的旧 key fallback 块。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 admin 刷新令牌 POST /admin/auth/refresh 行为变化 从字符串轮转改滑动续期,refresh 不换串
2 小程序刷新令牌 POST /mp/user/refresh 新增 小程序端首次有 refresh 能力
3 user-service 刷新令牌(内部) POST /user/refresh 新增 hl-mp-service Feign 调用入口
4 user-service 刷新令牌(MP 内部) POST /internal/mp/user/refresh 新增 mp-service → user-service Feign
5 admin 所有签发 token 接口 - login/2fa/switch-role/wechat-qr/check 等 响应字段微调 data 内仍是 token+refreshToken+accessTokenExpiresIn+refreshTokenExpiresIn,额外多 accessToken 字段
6 小程序 user login + login-by-sms POST /mp/user/login 响应新增 data 多 refreshToken+双 expiresIn+accessToken 字段

三、登录响应字段(灰度期前端可零改动)

{
  "code": 200,
  "data": {
    "token": "eyJ...ACCESS",                  // 向后兼容字段, = accessToken
    "accessToken": "eyJ...ACCESS",            // 新字段, 同 token
    "refreshToken": "eyJ...REFRESH",          // 新字段, 长效 7d/90d
    "accessTokenExpiresIn": 1800,             // 秒
    "refreshTokenExpiresIn": 604800,          // 秒, admin=604800/user=7776000
    "...其它现有字段保持不变"
  }
}

灰度期内前端不改任何代码,继续读 data.token 即可正常登录使用,只是 30 分钟后会被踢(因为没用 refresh)。要做无感续期才需要新增 refresh 拦截器。


四、新接口详情

1. POST /admin/auth/refresh (管理端)

// Request
{ "refreshToken": "eyJ...REFRESH" }

// Response 200
{
  "code": 200,
  "data": {
    "token": "...新 access...",
    "accessToken": "...新 access...",
    "refreshToken": "...原 refresh,字符串不变...",
    "accessTokenExpiresIn": 1800,
    "refreshTokenExpiresIn": 604800
  }
}

// Response 401 — refresh 已过期 / 已被 logout 作废 / 篡改 / tokenKind 错
{ "code": 401, "msg": "刷新令牌无效或已过期,请重新登录" }
  • 不需要 Authorization 头(网关 SKIP_URLS 已加白名单)
  • refresh JWT 字符串保持不变,仅刷新 Redis 中 jti 的 TTL
  • 错误码: 200209 REFRESH_TOKEN_INVALID / 200210 REFRESH_TOKEN_REPLAY / 200211 TOKEN_KIND_MISMATCH

2. POST /mp/user/refresh (小程序端,新接口)

请求 / 响应结构同上,refreshTokenExpiresIn = 7776000 (90 天)。

  • 不需要 Authorization 头
  • 链路: 小程序 → hl-mp-service MpUserControllerMpUserAggregationServiceMpUserFeignClient.refreshToken() → hl-user-service InternalMpUserController POST /internal/mp/user/refresh

3. logout 行为(无契约变化,但行为补强)

POST /admin/auth/logout / POST /mp/user/logout 后端同时清:

  • access 白名单 key (新+旧两套)
  • refresh 白名单 key

logout 后立即调 refresh 会 401。


五、自动 refresh 拦截器实现要点 (@mmg)

admin 端 (axios)

// 单飞锁防并发 401 一起触发多次 refresh
let refreshing = null;

async function onResponseError(err) {
  // 本项目 HTTP 总是 200,401 在 body.code,也要拦
  const code = err.response?.data?.code;
  if ((code === 401 || err.response?.status === 401) && !err.config._retried) {
    if (!refreshing) {
      refreshing = (async () => {
        const r = await axios.post('/admin/auth/refresh', {
          refreshToken: localStorage.getItem('admin_refresh')
        });
        if (r.data?.code !== 200) throw new Error('refresh failed');
        // 关键: refreshToken 字符串和原来一样,但 accessToken 变了
        localStorage.setItem('admin_access', r.data.data.accessToken); // 或 .token
        // refreshToken 不需要更新(后端不换串),但更新一下也无害
        localStorage.setItem('admin_refresh', r.data.data.refreshToken);
      })().finally(() => { refreshing = null; });
    }
    try {
      await refreshing;
      err.config._retried = true;
      err.config.headers.Authorization = `Bearer ${localStorage.getItem('admin_access')}`;
      return axios.request(err.config);
    } catch (refreshErr) {
      localStorage.removeItem('admin_access');
      localStorage.removeItem('admin_refresh');
      router.push('/login');
    }
  }
  throw err;
}

小程序端

同样的 401 拦截思路,把 axios 换成 wx.request 包装,storage 用 wx.setStorageSync('user_refresh', ...),refresh 端点 /mp/user/refresh

⚠️ 复用同一 refreshToken 字符串 — 后端不换串,你前端无需更新 refreshToken,只需更新 accessToken / token 字段。这与 PR #2285 的行为不同。


六、安全注意

  1. refreshToken 不要带在业务请求里,只在调 /admin/auth/refresh/mp/user/refresh 时用
  2. refreshToken 不要传给 Authorization 头 — 网关会显式拒绝(tokenKind=REFRESH 不能用于业务接口,401)
  3. logout 时要把 storage 里的 token + refreshToken 都清掉
  4. accessToken 30 分钟 TTL,即使被泄露窗口期短;refreshToken 7d/90d TTL 长,泄露后果严重 — 优先存 secure storage(小程序 wx.setStorage 即可,管理后台用 httpOnly cookie 不现实就用 localStorage,禁止打印到 console / 上传到第三方 SDK)
  5. switchRole / changePassword / 改密 / 删 admin / 锁定账号 等场景后端会主动清 refresh,前端调 refresh 收到 401 时直接跳登录即可

七、灰度兼容期具体行为(2026-05-15 ~ 2026-05-22)

场景 行为
旧前端 (只读 data.token,不调 refresh) ✓ 正常工作
旧前端 access 30 分钟后过期 ⚠️ 30 分钟后 401,被迫重登(因为没用 refresh)
新前端 (调 refresh) ✓ 滑动续期,7d/90d 内活跃永不重登
灰度期内旧 admin_token / user_token Redis key ✓ 网关 fallback 仍认
2026-05-22 后旧 key 失效 仍读旧 key 的代码会全失败 — mmg 务必在 5-22 前完成切换

八、不影响范围

  • 现有所有业务接口契约(参/返)零变化
  • 登录端点 path 不变
  • /admin/auth/info / /mp/user/profile 等读接口不变
  • 已签发的旧 access token 在 Redis 还有原 TTL,部署后不会立即失效(灰度 fallback)
  • 接口字段名 data.token 保留,不做 breaking 改名

九、配置项

配置 key 默认 备注
auth.jwt.admin-access-ttl-seconds 1800 (30 分钟) nacos 可覆盖
auth.jwt.admin-refresh-ttl-seconds 604800 (7 天) nacos 可覆盖
auth.jwt.user-access-ttl-seconds 1800 (30 分钟) nacos 可覆盖
auth.jwt.user-refresh-ttl-seconds 7776000 (90 天) nacos 可覆盖

代码层有 fallback,nacos 不配也能跑。


十、相关历史 PR

PR Issue 说明 是否仍有效
#2285 (2026-05-14) #2283 首版双 token,admin only,字符串轮转 已被本 PR 演进替代(除合规清理逻辑外行为差异)
#2287 hotfix - user-service TokenInterceptor 白名单 仍有效
#2294 hotfix - deleteAdmin/resetPassword/账号锁定 cleanup 仍有效,本 PR 兼容
本 PR #2312 #2309 滑动续期 + 双端覆盖 + 灰度兼容 最新

十一、联调端点

  • 测试服: https://api.test.1814.love/(待 Deploy Panel 部署 PR #2312 后生效)
  • 测试 admin: admin / Admin@123456
  • 测试小程序 user: 用现有微信测试号登录即可

十二、相关文档

  • 关联 Issue: wx/HL#2309
  • 关联 PR: wx/HL#2312
  • 上一版 changelog: 2026-05/14_feat_admin_auth_refresh_token.md