401静默重登-前端需求.md 5.3 KB

小程序 401 静默重新登录 - 前端需求

一、背景

生产环境 JWT 密钥发生过变更,老用户本地缓存的 token 已失效。当前老用户打开小程序后,所有需要登录的接口均返回 401,前端未处理 401,导致页面渲染空白。

结论:这是纯前端问题。后端 401 响应规范(HTTP 401 + JSON body),前端补齐 401 处理逻辑后,老用户即可"无感自动重新登录",无需弹窗提醒。


二、后端接口约定(已确认)

1. 401 响应格式

HTTP 状态码 401,响应体(JSON):

{ "code": 401, "message": "未授权,请先登录", "data": null, "timestamp": 1234567890 }

被踢下线(在其他设备登录)时:

{ "code": 1304, "message": "您的账号已在其他设备登录,当前会话已失效,请重新登录", "data": null, "timestamp": 1234567890 }

判定依据HTTP status === 401,或响应体 code === 401 || code === 1304

2. 静默重新登录接口(公开接口,无需鉴权)

POST /api/auth/wechat/miniapp/login
Content-Type: application/json

请求体:

{
  "jsCode": "wx.login() 返回的 code",
  "deviceType": "MINIAPP"
}

成功响应:

{
  "code": 200,
  "message": "操作成功",
  "data": {
    "accessToken": "...",
    "refreshToken": "...",
    "expiresIn": 2592000,
    "needBindPhone": false,
    "newUser": false,
    "userInfo": {}
  },
  "timestamp": 1234567890
}

说明

  • 该接口在公开白名单内,即使当前 token 失效也能正常调用。
  • 统一登录按 openid 自动复用老账号,不会新建账号、不会丢失用户数据
  • needBindPhone = true 时表示需要绑定手机号(老用户一般已绑定,正常返回 false)。
  • expiresIn 单位为秒。

三、前端实现方案

请求封装层(axios / request / 拦截器)增加 401 处理,核心逻辑如下。

核心代码(示意)

// 全局单例锁,防止多个请求同时 401 时重复触发重登
let refreshing = null;

async function handle401(error) {
  // 1. 若已有重登在进行,等它完成后直接重试
  if (refreshing) {
    await refreshing;
    return retryRequest(error.config);
  }

  refreshing = (async () => {
    try {
      // 2. 用 wx.login 获取新的 code(Promise 化)
      const code = await wxLogin();

      // 3. 静默重新登录(该请求不带旧 token,且不进入 401 拦截)
      const res = await request({
        url: '/api/auth/wechat/miniapp/login',
        method: 'POST',
        data: { jsCode: code, deviceType: 'MINIAPP' },
        skipAuth: true
      });

      if (res.code === 200 && res.data) {
        // 4. 保存新 token(覆盖本地缓存)
        saveToken(res.data.accessToken, res.data.refreshToken);

        // 5. 需要绑定时才走绑定流程
        if (res.data.needBindPhone) {
          await bindPhone(res.data.tempToken);
        }
      } else {
        // 登录接口本身失败,才真正引导用户重登
        redirectToLogin();
      }
    } catch (e) {
      redirectToLogin();
    } finally {
      refreshing = null;
    }
  })();

  await refreshing;
  // 6. 用新 token 重试原请求
  return retryRequest(error.config);
}

function wxLogin() {
  return new Promise((resolve, reject) => {
    wx.login({
      success: (res) => res.code ? resolve(res.code) : reject(new Error('no code')),
      fail: reject
    });
  });
}

拦截器接入(示意)

request.interceptors.response.use(
  (response) => {
    const res = response.data;
    if (res.code === 401 || res.code === 1304) {
      return handle401({ config: response.config });
    }
    return res;
  },
  (error) => {
    if (error.response && error.response.status === 401) {
      return handle401({ config: error.config });
    }
    return Promise.reject(error);
  }
);

四、必须满足的要点

  1. 静默无感:收到 401 时不弹窗、不跳登录页,直接执行 wx.login → miniapp/login → 重试原请求
  2. 防并发:多个请求同时 401 时,只触发一次重登(使用全局单例 Promise 锁,见上文 refreshing)。
  3. 兜底提示:仅当 miniapp/login 本身失败(网络异常、code 过期等)时,才提示"登录已失效,请重新进入小程序",并清空本地 token 引导重登。
  4. token 存储:重新登录成功后,accessTokenrefreshToken 需覆盖本地旧值;后续请求自动携带新 token。
  5. 公开接口豁免miniapp/login 请求本身必须标记为跳过鉴权(不携带旧 token、不进入 401 拦截),否则会死循环。

五、为什么用静默重登而非弹窗提醒

  • 小程序登录是无状态的(wx.login 拿 code 即可换 token),用户完全无感知,体验最好。
  • 弹窗提醒需要用户手动操作,容易造成用户流失。
  • 该方案为一次性:token 换新后,只要密钥不再变更,就不会再次触发。

六、验收标准

  1. 老用户(本地已缓存旧 token)打开小程序,页面正常渲染,无空白、无弹窗。
  2. 老用户刷新页面 / 重复进入,仍能正常使用,无需重新登录。
  3. 弱网或多接口并发场景下,不会出现重复登录请求。
  4. 服务端断网 / 登录接口异常时,有友好兜底提示,而非白屏。