news 2026/10/9 3:37:39

登录态复用与token机制详解:从双token到SSO无感刷新

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
登录态复用与token机制详解:从双token到SSO无感刷新

每次打开后台系统都要重新输一遍账号密码,切到另一个系统又得再来一次,找密码、收验证码、等短信,一天下来光登录就耗掉好几分钟。更难受的是,明明刚登录过,点个链接跳转另一个子系统,又让重新登录。这种体验不仅烦,放在业务上就是实打实的效率损耗。我自己经历过几次这种项目改造,后来仔细梳理了一遍token复用这件事,发现很多团队根本没有真正理解"登录态复用"该怎么做。

这篇文章就围绕"只登录一次、自动复用token"这个主题,把token的生成、存储、携带、刷新、跨系统复用这一整条链路拆开讲清楚。内容适合正在做前后端分离项目的全栈工程师、企业内部系统开发的同学,也适合那些想给多套系统统一登录体验的产品和技术负责人。文章会从原理讲到可落地的代码,再给一份实战排查清单,保证你看完能直接动手改自己的项目。

1. 先把"只登录一次"这件事拆清楚

1.1 每天登录三回,到底在哪一环浪费了时间

绝大多数登录场景的根源问题不是"登录"本身,而是登录态没有复用。很多团队是这样做的:登录成功后把token往localStorage一放,后面每个接口手动加一下请求头,看起来能用了,但真正用到极端场景就露馅。

场景一,用户打开多个子系统。比如一个公司内部有订单系统、报表系统、工单系统,三个系统各自独立部署。用户登录A系统拿到token,这个token只在A系统的存储里待着,切到B系统时B完全不认识他,于是又登录一次。三个系统就是三次登录,如果外加一个后台管理端,四套密码。

场景二,token过期时间设计不合理。假设access token有效期只有30分钟,用户在订单系统停留了40分钟,之后再点按钮,直接弹出token失效,只能重新登录。体验崩塌的瞬间,用户不会觉得是token机制复杂,只会觉得系统烂。

场景三,刷新页面后登录态丢失。前端的SPA应用,用户把页面刷新一下,如果登录态只存在内存变量里,刷新之后内存清空,token没了,又要重新登录。

这些问题的本质是同一个:登录态没有形成一条"复用链路",而是被当成一次性消费品。所谓"只登录一次",真正的技术含义不是把token存下来那么简单,而是要让登录态在有效期内自动携带、主动续期、跨应用共享,全程不需要用户介入。

1.2 选对复用方案之前,先理解登录态的本质

token这个词在前后端分离的项目里出现频率极高,但很多同学对token的理解停留在"一段放了就能调接口的字符串"。

现实中,登录态必须回答三个问题:你是谁(身份标识)、你有什么权限(访问范围)、现在是否有效(生命周期)。传统的session是把这三个问题的答案存在服务器内存里,每次请求带着session id去查;而token体系通常是无状态的,服务器通过签名校验来判断token是否可信。

复用token的前提,是搞清楚token的生命周期结构。现在业界通用的做法是双token体系:一个短期有效的access token,用于真正访问业务接口,通常15分钟到2小时过期;一个长期有效的refresh token,用于在access token过期后换取新的access token,通常7天到30天过期。

打个比方,access token像是酒店房卡,出门吃个饭回来发现房卡过期了,不需要去前台重新办入住,拿着身份证(refresh token)到前台刷一下就能补一张新卡。如果没有refresh token,每次房卡到期都得重新办入住(重新输入账号密码),这就是"明明登录过却又要重新登录"的技术原因。

1.3 为什么说"复用"比"存起来"更重要

单纯把token存起来,只能解决"刷新页面后token还在"这一件事。真正的复用,至少包含四个层面:

第一层,时间维度的复用。同一个token在有效期内可以重复使用,不需要每次请求前都去登录,这意味着要有一套"过期自动刷新、刷新失败才重新登录"的机制。网上很多教程只教了怎么把token存起来,没教怎么处理过期,导致token存了等于没存,用户用着用着还是被强制踢回登录页。

第二层,空间维度的复用。同一个用户在同一个浏览器的不同标签页、不同子系统之间、不同域名下,登录态应该能共享,至少不冲突。这涉及存储方案选型,比如localStorage是按域名隔离的,两个子系统如果域名不同,各存各的token,天然无法共享。

第三层,接口层面的复用。前端在发起请求时自动把token加到请求头里,后端能从请求头里解析出用户身份。这一层需要统一的请求封装,如果每个页面各自处理,代码量膨胀不说,还很容易漏加token导致401。

第四层,会话层面的复用。用户退出登录、切换账号、token被吊销等情况,需要在所有相关系统里同步清理登录态,否则会出现A系统已经退出,B系统还保持着"已登录"状态的尴尬情况。

所以后面讲的所有方案,都是围绕这四层展开的。不是给一个函数让你存token,而是把这四层全部打通。

2. 核心细节解析:token体系怎么设计才不会一改就崩

2.1 双token结构:短时效令牌加长时效刷新令牌

我在实战里推荐的设计方案是:access token用JWT格式,有效期设15分钟;refresh token使用不透明随机串,有效期设7天,并且强制轮换。

为什么access token有效期要这么短?有个现实考量是安全性:access token一旦被截获,攻击者能在有效期内冒充用户调用接口。15分钟把风险窗口压到足够小。配合refresh token机制,用户体验完全不受影响,因为刷新是无感的。

refresh token使用不透明随机串而不是JWT,原因在于refresh token一旦签发就不该被前端解读,它只是一个凭证。服务端要维护一张refresh token表,记录每个refresh token对应的用户、过期时间、是否已经使用过。每次使用refresh token换取新的access token时,服务端必须做两件事:标记旧refresh token失效,签发一个新的refresh token。

这就是refresh token轮换机制。为什么要轮换?如果refresh token可以反复使用,攻击者拿到一个refresh token就拿到了长期入口。轮换之后,每次刷新都会废弃旧的,攻击者要么用不了,要么一用就留下痕迹。

还有一点需要特别提一下:登录态刷新时,服务端需要能区分"同一个用户连续多次刷新"和"同一份refresh token被多处使用"。前者是正常行为,后者大概率是token被复制了。我一般会在refresh token表里记录最后一次使用的设备指纹和IP,一旦发现同一token短时间内在不同设备上使用,立刻吊销整条登录链。

access token我推荐在负载里带着用户id、角色、权限版本号,但不要放敏感信息。JWT的好处是服务端不用查库就能拿到用户身份,缺点是没法主动吊销。所以权限变更的场景,单靠JWT是不够的,需要叠加权限版本号校验。比如用户角色从普通成员变成管理员,权限版本号变了,服务端在网关层比对JWT里的版本号和当前版本号,不一致就拒绝并强制刷新token。

2.2 前端存储选型:localStorage、sessionStorage还是cookie

这个决策卡住过很多人。我先给一张对比表,再讲我的选择逻辑。

存储方案生命周期作用域主要风险适用场景
localStorage持久,除非手动清除同一域名下所有标签页共享XSS攻击可读取,明文暴露低频敏感数据,可配合加密
sessionStorage关闭标签页即清除单个标签页隔离多标签页登录态不共享临时数据、非登录态场景
内存变量页面刷新即丢失当前页面上下文刷新页面登录态丢失短期缓存、结合storage双读
cookie(httpOnly)受Expires/Max-Age控制受Domain和Path控制CSRF风险,需配合SameSite适合刷新token、服务端会话标识

我现在的做法是:access token存在内存变量里,同时写一份到localStorage做恢复用;refresh token放在httpOnly cookie里,前端JavaScript读不到。这样做有几个实际好处:

内存变量里存access token,刷新页面后localStorage里还能恢复,体验不丢。而httpOnly cookie里的refresh token,XSS脚本拿不到,安全性大幅提升。很多团队把两个token都塞localStorage,一旦站点被注入脚本,两个token一起被偷走,跟没做安全防护一样。

接入cookie的话,需要设置SameSite=Lax和Secure标记,同时Domain按需配置。如果多个子系统要复用登录态,cookie的Domain要设成父级域名,比如auth.example.com、order.example.com、report.example.com,cookie的Domain设为example.com才能跨子域携带。

有一点容易被忽略:cookie有大小限制(约4KB),JWT如果塞太多自定义字段,放到cookie里会溢出来。所以进cookie的refresh token必须是不透明短随机串,access token仍然通过请求头发送。

2.3 自动携带与401兜底:拦截器的正确写法

前后端分离项目里,请求层是token复用的主战场。以axios为例,我通常这样封装:

import axios from 'axios'; import { refreshToken, getAccessToken, setAccessToken } from './auth'; const http = axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 }); // 请求拦截:自动携带access token http.interceptors.request.use((config) => { const token = getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 是否正在刷新token的标记 let isRefreshing = false; // 等待刷新完成后的请求队列 let pendingQueue = []; function flushPending(error = null) { pendingQueue.forEach(({ resolve, reject }) => { error ? reject(error) : resolve(); }); pendingQueue = []; } // 响应拦截:401兜底,自动去刷新token http.interceptors.response.use( (response) => response.data, async (error) => { const { response, config } = error; if (!response || response.status !== 401 || config.__retried) { return Promise.reject(error); } // 如果已经有请求在刷新token,就把当前请求挂起 if (isRefreshing) { return new Promise((resolve, reject) => { pendingQueue.push({ resolve, reject }); }).then(() => { config.headers.Authorization = `Bearer ${getAccessToken()}`; return http(config); }); } isRefreshing = true; config.__retried = true; try { const { accessToken } = await refreshToken(); setAccessToken(accessToken); flushPending(); config.headers.Authorization = `Bearer ${accessToken}`; return http(config); } catch (refreshError) { flushPending(refreshError); // 刷新失败,跳转登录页 redirectToLogin(); return Promise.reject(refreshError); } finally { isRefreshing = false; } } );

这段代码是我自己在项目里反复打磨过的,几个细节值得说清楚。

第一,config.__retried标记必须加,否则刷新失败后返回的401还会再次触发刷新逻辑,形成死循环。

第二,并发请求时,多个接口同时返回401,如果没有isRefreshing开关,每个接口都会单独去刷新token。refresh token是轮换制的,第一个请求刷新成功,旧refresh token作废,第二个请求拿着同一份旧refresh token去刷新,必然失败,最终所有并发请求全部挂掉。加上开关和队列之后,只发一次刷新请求,其他请求排队等待新token。

第三,刷新失败后,flushPending(refreshError)先把队列里的所有请求拒绝掉,再跳转登录页。如果先跳登录再拒绝队列,部分请求可能触发未处理的promise异常。顺序不能反。

第四,401不一定都是token过期,后端接口自身的业务异常也可能返回401。所以响应拦截里最好加一个白名单判断,比如某些公开接口返回401就直接放行。我通常在后端返回体的code字段里区分:code === 40101表示access token过期,code === 40102表示refresh token过期。这样拦截器只拦截"token过期"的情况,避免误伤。

3. 实操过程:给三套内部系统做统一的"只登录一次"

3.1 场景设定与整体流程

假设现在有三套内部系统:管理系统(manage.example.com)、工单系统(work.example.com)、报表系统(report.example.com)。它们共用一套用户体系,希望用户在任何一套系统登录后,访问其他系统时不再需要输入账号密码。

这是典型的SSO场景,统一认证中心(auth.example.com)负责登录,三个业务系统共享登录态。整体的登录复用流程可以这样设计:

第一步,用户访问管理系统。管理系统前端检查本地没有有效token,就把用户重定向到认证中心,带上一个callback参数,比如https://auth.example.com/login?redirect=https://manage.example.com/callback。

第二步,认证中心展示登录页。用户输入账号密码或扫码,认证中心校验通过后,生成access token和refresh token,并且把session id写入认证中心的会话存储。随后将用户重定向回业务系统的callback地址,同时携带一个一次性授权码code。

第三步,业务系统的回调接口收到code,把它作为凭证发送到认证中心的token接口换取真正可用的token。这一步必须在后端完成,因为code换token的请求涉及敏感信息,不能在前端发起。

第四步,业务系统拿到token后返回给前端,前端按之前的方案存储和携带,业务接口正常使用。

这套流程里最核心的机制是:认证中心生成的会话可以跨系统复用。用户登录过一次,认证中心的会话里就记住了他的身份状态。当他访问第二个系统时,被重定向到认证中心,认证中心发现会话依然有效,直接从登录页变成"确认授权页"或者直接跳回,中间不需要再次输入账号密码。

3.2 从登录请求到登录态落库

认证中心的登录接口返回结构,我建议如下设计:

{ "code": 0, "message": "ok", "data": { "accessToken": "eyJhbGciOi...", "refreshToken": "xv7kq9f2mz4p", "expiresIn": 900, "refreshExpiresIn": 604800, "sessionId": "sess_8f3a2b9c" } }

accessToken是JWT,有效期900秒(15分钟);refreshToken是随机串,有效期604800秒(7天);sessionId是服务端会话标识,用于记录当前登录状态。

前端拿到这个响应后,做三件事:

第一,把accessToken写入内存变量和localStorage的某个固定key,比如auth.accessToken。

第二,把refreshToken交给浏览器存储为httpOnly cookie。这里不能直接让前端读取refreshToken再手动存储,正确做法是让认证中心在登录接口的响应头里通过Set-Cookie下发。前端代码里完全不用触碰refreshToken,它天然存在httpOnly cookie里,后续接口请求自动携带。

第三,把这个登录会话在浏览器层面记录下来。我用的是localStorage里的auth.sessionId,配合storage事件监听,后面讲多标签页同步时会展开。

服务端侧,认证中心需要维护一张会话表。表结构至少包含:sessionId、userId、refreshToken哈希、accessToken的jti、过期时间、设备指纹、IP、最后活跃时间。这里有个惯例:refreshToken不存明文,存哈希值,防止数据库泄露后token被直接盗用。

3.3 跨系统复用登录态:跳转拿到code再换token

跳转式SSO里有个细节必须处理好:code只能使用一次,有效期设为5分钟,过期作废。认证中心签发code后,把它与sessionId绑定,存入短期缓存。业务系统拿code来换token时,认证中心必须校验code存在、未使用、未过期,校验通过后立即标记为已使用。

如果业务系统拿同一个code换两次token,第二次必然失败,此时说明有人截获了code,安全策略应当立刻吊销对应的sessionId。

我在实际项目里写过的核心逻辑大致是:

/** 告诉你怎么设计,不直接给一坨能跑但并不好看的代码 */ // 1. 认证中心登录成功后,生成session,再生成一个一次性的code String code = generateRandomCode(); shortLiveCache.put(code, sessionId, 5, TimeUnit.MINUTES); // 2. 业务系统回调时携带code,认证中心用code换token if (shortLiveCache.get(code) == null) { throw new AuthException("code invalid or expired"); } String sessionId = shortLiveCache.get(code); Session session = sessionRepository.findById(sessionId); shortLiveCache.delete(code); // 使用后立即删除 String accessToken = issueAccessToken(session); String refreshToken = issueRefreshToken(session);

这里有个容易被忽略的坑:业务系统回调时如果是在服务端发起HTTP请求去换token,网络环境里可能遇到代理拦截、超时等问题,导致code已经签发但令牌没换到。用户再次尝试时,code已经作废,会卡在"登录失败"上。我处理方式是,换token失败时不立即作废code,而是允许业务系统在5分钟内重试,重试次数限制为3次。一旦重试成功,代码路径和执行正确。

另一种跨系统复用的实现思路是iframe + postMessage。认证中心在一个隐藏iframe里,业务系统通过postMessage向认证中心询问"当前用户是否已登录",认证中心返回登录态。这种方式能实现页面不跳转的静默登录,但受浏览器跨域策略限制,实现复杂度高,通信时序也容易出现问题。我建议优先采用跳转式,先保证稳定再说体验。

3.4 无感续期:在用户还没察觉时完成token刷新

token复用最理想的状态是:用户从上班登录一次,到下班离开,中间完全感知不到token的存在。要达到这个效果,仅靠"401时再刷新"还不够,因为刷新过程有短暂延迟,接口还是会慢几毫秒,而且极端情况下如果用户在一个页面停留超过refresh token有效期,还是会掉登录。

我给项目设计的方案是定时预刷新。前端拿到access token的过期时间后,设置一个定时器,在过期前60秒主动调用刷新接口,把旧access token换成新的。举个例子:access token有效期为15分钟,第13分钟时前端发起刷新请求,第14分钟时新的access token已经就位,用户的连续操作无感知。

定时预刷新的核心代码如下:

let refreshTimer = null; function scheduleRefresh(expiresIn) { clearTimeout(refreshTimer); // 过期前60秒执行刷新 const delay = Math.max(0, (expiresIn - 60) * 1000); refreshTimer = setTimeout(async () => { try { const { accessToken, expiresIn: newExpiresIn } = await refreshToken(); setAccessToken(accessToken); scheduleRefresh(newExpiresIn); } catch (e) { // 刷新失败不急着跳转,等用户下一次操作时由401兜底 console.warn('token refresh failed, waiting for next interaction'); } }, delay); }

定时刷新有个风险:用户已经关闭了浏览器标签页,但定时器还挂在后台。所以页面在进入后台状态时应该清理定时器,回到前台时重新计算剩余时间。监听visibilitychange事件,页面隐藏时清掉定时器,显示时根据当前时间推算剩余有效期,再决定是否立即刷新。

还有一点经验:刷新请求的时序非常关键。如果预刷新请求刚发出去,用户同时点了某个按钮触发了业务请求,这时候access token还没更新,请求带的是旧token,后端校验过期,返回401。前端响应拦截器又开始走刷新流程,这时定时器那边也在刷新,两个刷新并发。所以刚才封装响应拦截器时的isRefreshing开关必须同时被定时器复用,不能只在401路径里生效。

4. 常见问题与排查技巧实录

4.1 token失效问题大清单

很多同学遇到"token失效"第一反应是看后端日志,但我排查这类问题的经验是:先从前端存储、请求头、时间偏差三个维度快速定位,再决定要不要深入后端。下面这是我在实战中积累的排查速查表。

现象可能原因排查步骤
请求报401,响应头没有新token前端没有正确携带Authorization头打开浏览器开发者工具,查看请求头里有没有Authorization: Bearer xxx,没有就是请求拦截器没生效
带上了token仍然401token过期把token拿到jwt.io解析,看exp字段是否已过当前时间戳
刷新token也失败refresh token被轮换废弃查看当前请求使用的refresh token与上一次成功刷新的返回是否一致,一致则说明刷新接口没走轮换逻辑
明明登录成功了,刷新页面又跳到登录页access token没写localStorage,只放内存检查登录成功后存储逻辑
多标签页,一个标签页退出,另一个标签页还显示已登录缺少storage事件监听在非退出标签页监听storage的sessionId变化,发现变化则清理登录态
访问另一个子系统被重复要求登录统一登录态作用域未覆盖该子域检查cookie的Domain配置,以及localStorage按域名隔离的特性
access token没过期但接口返回401服务器时钟与前端时钟不一致对比服务器时间与token签发时间,超过偏差调整NTP同步
修改密码后旧token还能用缺少token版本或权限版本校验在JWT负载里加pw_version字段,密码修改时递增,服务端比对

这里特别说一下"token exchange failed"这类报错。我平时排查时发现,这类问题多半发生在"用授权码换token"这一步,也就是前面SSO流程里业务系统拿code调认证中心token接口的环节。典型的失败原因有:code已经被使用过、code过期、发起token交换的请求不是从业务系统后端发起的而是被前端误调了、网关拦截了token接口的请求。排查方向先确认code的生命周期,再看请求来源是不是符合预期,最后检查网络代理层有没有过滤特殊参数。

4.2 并发刷新导致refresh token被轮换掉

这是我在生产环境里踩过最深的一个坑,值得单独讲。早期版本没有做单飞处理,有一天我突然发现日志里有大量"refresh token not found"的报错,一查是并发刷新导致的。

场景是这样的:用户在一个页面里,前端同时加载三个模块,瞬间发出三个业务请求。三个请求都带着即将过期的token,后端全部返回401。响应拦截器被触发三次,三个拦截器同时调用refreshToken函数。第一次刷新成功后,旧的refresh token被轮换失效;第二次、第三次刷新还在用旧refresh token,只能收到"token already used"的错误。

实际上解决方案在第二节已经给出了:isRefreshing开关加请求队列。但这里我要再强调一个容易出错的细节:队列不能只存"等待的promise",还要存每个请求自己对应的config,否则刷新完成后,排队中的请求无法用新token重新发起。我的代码里pendingQueue存放的就是{ resolve, reject }两个回调,然后在flush的时候外层用http(config)重新发送,不要直接在原来的error里改动config,那样会造成响应对象错乱。

另一个方案是给刷新接口做并发去重,服务端对同一sessionId的并发刷新请求只放行一个,其余直接返回同一个新token。这样前端不用管理复杂队列,但要求后端刷新接口是幂等的,实现上比前端加开关更麻烦。我目前始终坚持前端单飞方案,效果已经足够稳定。

4.3 多系统之间复用不了:子域、存储隔离的坑

跨系统复用登录态,前端遇到的第一个坑就是存储隔离。localStorage的隔离规则是按协议、域名、端口三者共同确定的,manage.example.com下的localStorage在work.example.com里完全不可见。如果两个子系统的前端各自往localStorage里写token,它们永远不可能共享。

解决方案有两种。要么把token放进cookie并设置Domain为父级域名,让所有子域都能读取;要么用统一认证中心做SSO跳转,业务系统不直接保存token,而是通过认证中心换取各自的临时token。

我强烈推荐第二种。原因在于,cookie虽然能跨子域共享,但随着业务系统增多,所有系统都靠同一份cookie标识身份,一旦一个子系统被XSS攻击,攻击者可能通过cookie拿到统一登录态权限。而SSO方案里,每个业务系统有自己的token,虽然底层会话共享,但独立凭证让权限边界更清晰。

多子系统共享登录态时的另一个问题是:用户在A系统修改了密码,B系统里还在用旧密码对应的token。所以SSO方案必须支持会话失效广播。最简单的实现是认证中心提供一个"用户会话版本号",每次改密码或踢人下线时递增版本号,业务系统在每次请求时比对JWT里的版本号,发现不一致就走强制刷新,刷新后如果版本号仍然不一致就跳转登录。

4.4 强制下线与多标签页同步登录态

有时候管理员要强制某个用户下线,或者用户在别处登录后旧会话被踢掉,这时候"只登录一次"带来的副作用是:所有系统里的登录态都要一起失效。不然就会出现A系统已经断开,B系统还静默持有有效token的情况。

我的处理方式是引入会话状态事件。认证中心维护一个WebSocket通道或者用开源的SSE广播,当用户会话被吊销、密码修改、设备重新登录时,推送一个"session_invalid"事件。各业务系统接收到事件后,清理本地token。如果基建能力不足,可以用一个折中方案:前端每个页面启动一个定时器,每隔5分钟调用一次认证中心的check接口,返回的会话状态一旦变成invalid,就本地清理并跳转登录页。

多标签页同步相对简单。登录、退出、切换账号时都会写localStorage里的auth.sessionId,浏览器原生会触发storage事件,所有同域标签页都能监听到。在监听回调里对比sessionId是否变化,如果是退出事件就清理token,如果是新用户登录就重载页面数据。

window.addEventListener('storage', (e) => { if (e.key === 'auth.sessionId' && e.newValue !== e.oldValue) { if (!e.newValue) { // 清空凭证 clearTokens(); redirectToLogin(); } else if (e.newValue !== currentSessionId) { // 换了账号,刷新页面以重新拉取当前用户数据 window.location.reload(); } } });

注意这里只同步sessionId,不同步accessToken。因为accessToken本身有效期只有15分钟,同步过来的token可能已经过期,不如强制刷新页面,让内存token管理逻辑重新走一遍。

5. 只记住这一条:别做最省事但最坑的"无脑复用"

5.1 安全清单:复用token必须守住的红线

说了这么多"怎么让token生效时间更长、用得更方便",但token复用最怕的就是无脑复用。我整理了一份红线清单,是我自己踩过坑之后定下的规矩:

第一,access token不准放url里。有些系统为了图方便,在跳转链接里直接拼token,https://manage.example.com?token=xxx。这会让token出现在浏览器历史、服务器访问日志、第三方统计脚本的referrer里,等于主动把凭证送人。SSO跳转用的是一次性code,而不是token本身,就是这个原因。

第二,refresh token必须放httpOnly cookie,并且设置Secure和SameSite=Lax。没有httpOnly保护,前端一个XSS就能把refresh token读走,双token体系直接崩盘。

第三,refresh token必须一次性、轮换、过期。一次性防止重放攻击,轮换让旧token快速失效,过期时间不宜过长,7天是一个相对合理的平衡点。长时间不活跃的会话要允许被服务端回收。

第四,退出登录时要同时清掉access token和refresh token,并且调用服务端接口让refresh token在数据库里失效。只清前端存储、不撤服务端凭证,等于logout了个寂寞。如果还想更严谨,就把access token的jti加入短期黑名单,防止logout后token在剩余有效期内被滥用。

第五,生产环境的token密钥必须独立管理,定期轮换。很多团队把JWT密钥写在配置文件里,一放就是两三年,密钥泄露的后果是攻击者可以自己伪造任意用户的token。密钥管理建议使用专门服务,至少也要有定期轮换的自动化机制。

5.2 换设备、退出登录、账号切换怎么处理

换设备的场景其实指"同一用户在不同浏览器、不同电脑上保持登录状态"。正常情况下不需要额外处理,因为refresh token按设备独立签发,A设备上的登录不会影响B设备。但有一种常见需求是"一个用户登录后,新设备登录会把旧设备踢下线"。这就需要在认证中心记录设备指纹,登录时如果会话策略是单端登录,就把该用户其他设备的refresh token全部吊销。

退出登录的清理动作,我通常会写成这样一个工具函数:

function logout() { // 1. 通知服务端吊销会话 fetch('/api/logout', { method: 'POST', credentials: 'include' }); // 2. 清空本地内存和localStorage setAccessToken(null); setSessionId(null); // 3. 清除cookie里的refresh token,注意要匹配Domain和Path document.cookie = 'refresh_token=; Max-Age=0; Path=/; Domain=.example.com; SameSite=Lax; Secure'; // 4. 跳转登录页 window.location.href = 'https://auth.example.com/login'; }

第三步最容易出错。很多同学直接写document.cookie = 'refresh_token=; Max-Age=0',结果没带Domain和Path,生成的清除cookie跟原本的httpOnly cookie不匹配,旧cookie根本没被删掉,下次又带着refresh token去刷新,服务端那个会话明明已经吊销了,但又冒出个残留cookie。所以清除cookie时,设置项必须和创建时保持一致,Domain、Path、Secure、SameSite一个都不能少。

账号切换的本质,是在同一个浏览器里把当前登录态完全替换成新账号的登录态。切换前必须无条件调用logout逻辑,因为同名子域下的cookie不会因为你登录了新账号就自动替换,残留的旧账号cookie会和新的会话冲突。

5.3 我对这套token复用方案的最终体会

整套方案跑下来,我个人最大的体会是:token复用不是"写个拦截器"或者"存个cookie"这种单点动作,而是一条从登录到刷新到跨系统共享到安全回收的完整链路。

容易做崩的地方不在某个技术细节,而在边界情况。比如并发刷新、cookie清除、跨域跳转、多标签页同步,每一个单拎出来都简单,串起来才见功力。我在改造项目时发现,真正决定上线后体验好不好的,不是代码写得多优雅,而是有没有把"用户不会感知到token的存在"当作验收标准。

如果只能给一条建议,我会说:先把401兜底和无感续期做扎实,再碰SSO多系统共享。因为单系统的token复用是最小可行闭环,这个闭环不稳固,扩展到多系统时问题会成倍放大。等单系统跑顺了,再按文章第三节的跳转式流程去打通多系统登录态,你会发现之前积累的每段代码都能直接复用。

我在做这类改造的时候,还有一个小习惯:所有token相关的日志都带上sessionId和用户id,方便串联排查。没有日志的token系统,出了问题只能靠猜,效率会很低。这块建议项目一开始就埋好,别等上线之后再补。

这套方案不是银弹,但它能覆盖绝大多数"内部多系统反复登录"的痛点。你照着这个路子改造自己的项目,稳定性和体验都会有质的提升。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 3:37:37

边缘计算新十年:从比特到原子的边缘物理智能PIE

边缘计算喊了快十年,从最早“把计算放到离数据最近的地方”这个概念,到后来各种边缘平台、边缘智能框架层出不穷,绝大多数讨论其实还停留在比特层面——我们优化的是数据流、计算负载、模型精度、网络延迟。但施巍松教授团队这次提出的新十年…

作者头像 李华
网站建设 2026/10/9 3:37:18

用ENSP完成校园局域网课程设计:VLAN划分、DHCP配置与NAT出口全攻略

简介:基于eNSP的校园局域网课程设计报告文档,面向计算机网络专业学生和需要完成组网实训课程设计的人群,提供从需求分析到网络设计落地的完整参考方案。内容覆盖终端接入数量与位置分布、组网技术选型、带宽与子网划分要求、安全性需求&#…

作者头像 李华
网站建设 2026/10/9 3:37:18

多表查询JOIN实战指南:从连接类型选型到去重与性能优化

聊一个实际的问题:单表查询你写得再溜,一遇到真实业务基本撑不过半天。用户表、订单表、商品表、分类表,数据天生就是拆开存放的,你迟早得面对“两张表拼起来查”这件事——这就是多表查询。很多人学到第六章时开始犯怵&#xff0…

作者头像 李华
网站建设 2026/10/9 3:35:58

MES与ERP集成:让采购计划真正跟着生产消耗走

干了这么多年制造业信息化,最让我头疼的其实不是技术选型,而是车间里那点说不清道不明的账。计划员催采购,采购催供应商,供应商说交期就是下周三,结果下周三料没到,车间停线等着,老板在早会上拍…

作者头像 李华
网站建设 2026/10/9 3:35:53

Spark数据存取底层逻辑与读写调优:从文件格式到分区裁剪

1. 先还原一次"读数据慢"的排查:Spark存取的底层逻辑1.1 那个26分钟的作业,问题出在读的姿势前阵子帮同事排查一个离线数仓任务,作业本身逻辑非常简单:从Parquet表读订单明细,过滤最近7天数据,按…

作者头像 李华
网站建设 2026/10/9 3:35:38

PyTorch张量操作进阶:索引分片、合并与维度调整完全指南

刚开始用PyTorch跑模型的时候,我大部分时间都在跟报错搏斗,其中出现频率最高的不是loss爆炸,而是各种和shape有关的提示。后来才慢慢发现,与其说是模型结构复杂,不如说是在张量的索引分片、合并和维度调整这些基本功上…

作者头像 李华