生产环境 JWT 密钥发生过变更,老用户本地缓存的 token 已失效。当前老用户打开小程序后,所有需要登录的接口均返回 401,前端未处理 401,导致页面渲染空白。
结论:这是纯前端问题。后端 401 响应规范(HTTP 401 + JSON body),前端补齐 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。
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
}
说明:
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);
}
);
wx.login → miniapp/login → 重试原请求。refreshing)。miniapp/login 本身失败(网络异常、code 过期等)时,才提示"登录已失效,请重新进入小程序",并清空本地 token 引导重登。accessToken 与 refreshToken 需覆盖本地旧值;后续请求自动携带新 token。miniapp/login 请求本身必须标记为跳过鉴权(不携带旧 token、不进入 401 拦截),否则会死循环。wx.login 拿 code 即可换 token),用户完全无感知,体验最好。