Appearance
概览
目标:当会话过期、多端登录被挤下线或长时间未操作时,主动提示用户并引导重新登录。
核心思路:
- 服务端主动通知(WS/推送)+ 客户端统一处理。
- 客户端被动检测(拦截 401/403 或本地过期计时)+ 统一登出流程。
- AccessToken(短)+ RefreshToken(长) 的续期机制,尽量无感刷新;刷新失败再提示登录。
- WebSocket 在握手即鉴权,并约定失效信号与重连策略。
一、触发与检测机制
1. 服务端主动通知(实时感知)
典型场景:他端登录挤下线、管理后台强制失效、安全风控。
通道选择:
- WebSocket:向客户端推送
logout/session_expired事件;客户端弹窗 → 清理 → 跳登录。 - 移动推送(APNs/FCM):App 在前台时提示,后台可静默标记,回到前台统一处理。
- WebSocket:向客户端推送
优点:即时、统一;缺点:需协议与客户端处理逻辑配套。
2. 客户端被动检测(最常见)
- HTTP 拦截:在网络层统一捕获 401/403。
- 本地计时:解析 token 的
exp,到期前刷新,失败则触发登出。 - 优点:实现简单、集中维护;缺点:非实时(在发起请求时才发现)。
二、Web 应用实现与交互范式(React/Vue/SPA)
1. 统一拦截器
js
// 复杂逻辑:统一拦截 401,先清状态再引导登录,避免“幽灵会话”
import axios from "axios";
axios.interceptors.response.use(
(res) => res,
(err) => {
const status = err?.response?.status;
if (status === 401 || status === 403) {
// 清本地凭证与用户态
localStorage.removeItem("authToken");
// 可广播给其它标签同步退出(BroadcastChannel 或 storage 事件)
// new BroadcastChannel('auth').postMessage({ type: 'LOGOUT' });
// 友好提示 + 携带回跳
const redirect = encodeURIComponent(
window.location.pathname + window.location.search
);
window.location.href = `/login?redirect=${redirect}&reason=expired`;
}
return Promise.reject(err);
}
);2. 多标签页同步退出
- 使用
BroadcastChannel('auth')或window.addEventListener('storage')同步其它标签页的登出动作。 - 登出时只做一次性清理,其余标签页收到事件后跳登录即可。
3. 交互建议
- 前台弹窗(模态框/顶部警示条):提示“登录已失效,请重新登录”。
- 路由守卫:访问受保护路由时若无有效会话,直接重定向登录并带回跳参数。
- 可回跳:登录成功后返回原页面,降低打断感。
三、移动端实现与交互范式(iOS/Android/Flutter)
1. 网络层统一拦截
Android(OkHttp/Retrofit)示例:
java
// 复杂逻辑:网络层统一捕获 401,切至主线程做 UI 提示与登出跳转
class AuthInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Response res = chain.proceed(chain.request());
if (res.code() == 401 || res.code() == 403) {
App.runOnMainThread(() -> App.get().onTokenExpired());
}
return res;
}
}iOS(Alamofire/URLSession)要点:
- 在响应回调里判断 401/403 →
DispatchQueue.main.async切主线程弹窗与路由到登录页。 - 清理:Keychain/NSUserDefaults 中的 token、内存单例用户对象、与用户相关的后台任务。
2. 推送配合
- 前台:展示对话框并跳登录。
- 后台:标记“需重登”,用户回前台时统一处理(或使用静默推送直接触发登出流程)。
3. 导航与状态
- 清空导航栈/重建根路由,禁止返回到受保护页面。
- 退出时注销推送别名/话题订阅,避免误投递。
四、Token 最佳实践
1. 短 Access + 长 Refresh
- Access Token(15–60 分钟):用于请求;
- Refresh Token(天/周级):用于无感续期;
- 流程:请求失败(401)→ 先尝试刷新 → 刷新成功重试原请求 → 刷新失败才提示重新登录。
2. 安全与合规
- Web:Refresh Token 放 HttpOnly Cookie;Access 可放内存/非持久。
- 移动端:Refresh 放 Keychain/Keystore;必要时二次加密。
- Refresh Token 轮换:每次刷新下发新 RT,使旧 RT 失效。
- 强退/改密:后端需可撤销既发 token(黑名单/会话存储),并广播失效事件。
3. 客户端实现要点
统一的 token 管理器:
- 维护
access/refresh/exp; - 提供
getValidAccessToken()(自动判断与刷新); - 并发刷新去抖:刷新中其余请求等待同一 Promise,避免风暴;
- 最大会话总时长/刷新次数上限,超限则要求重新登录。
- 维护
五、WebSocket 鉴权与失效处理
1. 握手即鉴权
浏览器:URL 查询参数附带
token(配合wss://;避免在日志中记录完整 URL)。js// 复杂逻辑:用代次隔离迟到消息,切换/重连时避免“幽灵数据” let gen = 0, ws = null; function connect(token) { const myGen = ++gen; ws = new WebSocket( `wss://example.com/stream?token=${encodeURIComponent(token)}` ); ws.onmessage = (e) => { if (myGen !== gen) return; /* 正常处理消息 */ }; ws.onclose = () => { /* 根据 code/reason 决定是否提示或重连 */ }; }原生/Node 客户端:可走
Authorization: Bearer <token>自定义头。
2. 过期与续期
- 主动续期:本地检测将过期 → 刷新成功后 重建 WS(带新 token)。
- 服务端信号:约定
auth_error事件或特定 close code;客户端收到后弹窗 → 清理 → 跳登录或执行透明重连。 - 多端冲突:新连接建立 → 服务端关闭旧连接并推送“被挤下线”消息;客户端统一走登出流程。
六、统一登出流程(Web/移动端通用清单)
- 停止一切后续请求(可取消队列/挂起 UI 操作)。
- 清理本地凭证与用户态:token、用户信息、缓存、订阅、定时器。
- 关闭 WS/RTC/推送别名 等与用户绑定的长连接通道。
- 多实例同步(Web 多标签、移动多进程)。
- 跳转登录页(重建导航栈;可带回跳参数)。
- 显式提示原因:会话超时/他端登录/安全风险等。
七、常见陷阱与规避
- 只清 token 不清内存态 → 页面仍显示旧用户信息。
- 并发刷新风暴 → 使用单航道刷新(队列/锁/共享 Promise)。
- WS 不识别过期 → 缺少 close code/事件约定;补齐协议。
- 多标签不同步 → BroadcastChannel/storage 事件必配。
- Refresh 永不过期 → 增加最长会话窗口与轮换策略。
八、快速集成清单(落地指南)
- [ ] Axios/OkHttp/Alamofire 响应拦截器接管 401/403。
- [ ]
TokenManager:持久化、getValidAccessToken()、并发去抖刷新。 - [ ] Access+Refresh 流水线与轮换;Web 用 HttpOnly Cookie。
- [ ] WebSocket:握手鉴权、失效信号、重连与代次。
- [ ] 统一
logout():清理 + 关闭通道 + 同步 + 跳登录。 - [ ] 多端登录策略:后端单会话/多会话规则 + 主动通知。
- [ ] UX:显式原因、可回跳、不中断未保存数据(必要时草稿)。
附:代码片段索引
- Web:Axios 401 拦截(见上文「二-1」)。
- Android:OkHttp 拦截器(见上文「三-1」)。
- WS:握手鉴权与代次(见上文「五-1」)。
以上为可直接落地的基线方案;结合你们目前“HTTP 为主 + 服务器亦可下发退出通知”的架构,建议先完善客户端拦截与统一登出,再逐步接入WS 主动通知与Refresh 轮换,最终实现“无感续期,必要时清晰提示”的一致体验。