ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

前端登录退出全攻略:Token获取、缓存、失效处理与代码实现

前端登录退出全攻略:Token获取、缓存、失效处理与代码实现 干前端这么多年要说哪个环节最容易被低估登录退出这套流程绝对排得上号。很多项目上线后出的线上事故排查到最后往往不是什么高深算法而是Token在某个边界场景下没处理好Token过期了但刷新逻辑没覆盖用户多点了一次退出导致缓存清了一半或者刷新Token的接口被并发调用直接把服务端干懵。这个标题“前端登录退出处理Token问题获取、缓存、失效处理以及代码实现”看着普通但背后能挖出来的东西其实非常多。这篇文章我打算从真实项目里的踩坑经验出发把Token从获取、缓存到失效处理的完整链路拆开揉碎讲清楚附上可以直接抄走的代码实现希望能帮你在面试和被线上问题追着跑的时候都心里有底。1. 整体设计思路Token在前端登录退出里的定位1.1 为什么前端必须自己管好Token先理清一个根本问题HTTP协议本身是无状态的服务器不认识“你是谁”。为了让用户在刷新页面、跳转路由之后依然保持登录状态我们才需要在客户端存一个凭证每次请求时带上它让服务器能认出你。这就是Token存在的意义它本质上是一张“临时通行证”。很多人觉得Token处理不就是存一下、请求时带上、过期了弹个框吗实际上完整链路远不止这三步。一个合格的Token处理方案至少要覆盖五个环节获取登录时怎么拿到、缓存存哪里既安全又方便、携带每个请求怎么带上、失效处理过期了怎么办、要不要静默续期、清除退出登录时怎么彻底清理。这五步环环相扣任何一环出了问题用户感知最明显的表现就是“登录状态莫名其妙丢了”或者“明明登录了却一直提示登录过期”。我在多个项目里实测下来90%以上的Token相关问题都不是单个环节坏了而是环节之间的衔接出了问题。举个真实例子有的项目把Token存在localStorage里但登录接口是在axios拦截器初始化之前发出去的导致登录成功后Token写不进请求头第一个业务请求必然401。这类问题光看单段代码根本发现不了必须站在整条链路的角度审视。1.2 设计Token方案前必须先登记的五个问题动手写代码之前我强烈建议先回答下面五个问题它们直接决定你的技术选型和代码结构Token由谁签发是自己的后端服务还是第三方OAuth服务这决定了你能否控制Token的过期策略和刷新接口。后端期望前端以什么方式携带Token绝大多数是Authorization: Bearer token请求头也有少数项目要求放在自定义header里。有没有refresh_token刷新令牌如果没有Token过期后只能让用户重新登录体验会差很多。Token过期时间是多长2小时和7天对应的前端策略完全不同。过期时间短就需要一套稳健的静默续期机制。前端项目是SPA还是SSRSSR场景下Token还要考虑服务端注入存储位置的选择逻辑也不一样。把这些确认清楚再写代码能省掉后面大量返工时间。我见过太多项目一上来就照着网上某个模板封装axios结果后端的鉴权模型跟模板假设的根本不是一回事越往后改越痛苦。1.3 一套可复用的Token生命周期管理模型把所有方案抽象到底层其实前端Token管理就是一张状态机未登录 - 登录中 - 已登录有效 - 已登录过期续期中 - 未登录。前端所有代码的职责就是在这几个状态之间正确地流转。状态流转的触发点通常有三个路由守卫、axios响应拦截器、定时器。路由守卫负责“进入页面时判断有没有Token”axios拦截器负责“请求发出前带上Token、响应回来时识别失效”定时器负责“快到过期时间时提前续期”。三者配合得当用户感知到的就是“一直在登录状态无需重复输入账号密码”。这篇文章后面所有代码都是围绕这张状态机展开的。2. 获取Token登录交互与请求封装要点2.1 登录接口返回Token的两种交互形态获取Token这一步市面上主流方案大致分两种形态。第一种是登录接口直接返回Token前端拿到后自行保存后续请求手动加到header里。这种方案最直观也是大多数中后台项目采用的方式可控制性强前端想怎么存就怎么存。第二种是基于Cookie的会话机制后端通过Set-Cookie把会话标识写入浏览器前端代码层面几乎感知不到Token的存在请求会自动携带Cookie。这种方案的好处是浏览器帮你处理了自动携带代码简单但在跨域场景下要小心翼翼配置SameSite、CORS等属性而且CSRF防护成本更高。我的建议很简单只要后端接口设计上允许优先选第一种。原因不是Cookie方案不好而是前端项目一旦涉及多个域名、跨域调试、小程序或者App端复用基于Header的Token方案迁移成本最低排查问题也更直观——打开开发者工具就能看到每个请求带的Token是什么而Cookie和HttpOnly限制经常让人排查问题时无从下手。2.2 用axios实例统一登录态管理无论选哪种方案前端最终都需要一个封装好的请求模块。这里我把实际项目里沉淀下来的一份axios封装模板分享出来它解决了“统一携带Token”和“登录态丢失时统一跳转”这两个核心诉求import axios from axios import { getToken, clearToken } from /utils/auth import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }) // 请求拦截器自动附加Token service.interceptors.request.use( (config) { const token getToken() if (token) { config.headers[Authorization] Bearer ${token} } return config }, (error) Promise.reject(error) ) // 响应拦截器统一处理401和其他错误码 service.interceptors.response.use( (response) { return response.data }, (error) { const { response } error if (response response.status 401) { // 登录态失效清Token、跳登录页 clearToken() router.push(/login?redirect${encodeURIComponent(location.pathname)}) Message.error(登录已过期请重新登录) } else { Message.error((response response.data response.data.message) || 请求失败请稍后重试) } return Promise.reject(error) } ) export default service这段代码看着不复杂但有三个细节值得注意。第一clearToken()之后立即跳转登录页不要在这之前弹多个错误提示否则用户会看到一堆重复报错。第二跳转时带上redirect参数用户重新登录后可以直接回到原来的页面这个小细节对体验提升非常明显。第三请求拦截器里判断了getToken()存在才追加header避免未登录状态下所有请求都带着一个空的Bearer null过去有些后端会对格式不规范的header直接返回400。2.3 登录成功后要做的三件事顺序不能乱用户输入账号密码、点登录、接口返回Token之后前端需要做的操作我建议严格按这个顺序来保存Token到缓存。先存起来这是后续一切操作的基础。更新应用状态。把用户信息、登录状态写进Vuex/Pinia或全局状态里让页面UI能立刻响应登录成功。跳转页面。回到用户原本想访问的页面或者默认首页。顺序为什么不能乱如果你先跳页面再存Token新页面里有组件在onMounted里发请求此时Token还没落库请求拦截器拿不到Token就会产生“用户明明登录成功了但页面还是疯狂报401”的诡异现象。我在真实项目里踩过这个坑排查了半天才发现是登录回调里三行代码的先后顺序写反了。3. 缓存Token四种方案对比与选择建议3.1 从“四选一”到“应该用哪个”Token缓存的选型其实是前端安全与便利性之间的权衡。四个常见选项分别是localStorage、sessionStorage、cookie、内存变量。我看过不少文章把它们罗列成一平铺直叙的对比表格然后说“建议用localStorage”但真正决定选型的是你的应用面对的安全威胁模型和业务场景。localStorage持久化存储关闭浏览器后依然存在。容量5MB上下API简单但任何在页面里执行的脚本包括XSS注入的脚本都能通过localStorage.getItem(token)直接偷走Token。适合对安全要求不是极端苛刻、追求用户体验的普通后台管理系统。sessionStorage持久性只限于当前标签页关掉标签页Token就没了。好处是“刷新页面不丢、新开标签页要重新登录”适合一些希望限制多标签页登录场景的内部系统。Cookie如果后端依赖自动携带Cookie是绕不开的选择。设置HttpOnly可以防止脚本读取对XSS免疫性更强但要额外处理跨域、CSRF、SameSite等一堆问题。内存变量存在Vuex/Pinia或者一个普通对象里刷新页面即丢失安全性最高不落盘但用户体验最差——一刷新就要重新登录。3.2 我更推荐的组合内存 localStorage / sessionStorage在实际项目里我通常采用“内存为主、持久化为辅”的组合策略Token同时存在内存Pinia和localStorage或者sessionStorage视需求里。程序运行时优先从内存读取刷新页面后内存清空了再从localStorage回填到内存。为什么要绕这么一圈因为内存读取最快且不依赖浏览器存储API而localStorage负责跨页面刷新时的持久恢复。两者配合既不牺牲刷新体验又能避免每次读取都走磁盘I/O。这个模式本质上就是“写两份读一份”读永远走最快的那份。// auth.js —— 封装Token读写 const TOKEN_KEY access_token let memoryToken null export function getToken() { return memoryToken || localStorage.getItem(TOKEN_KEY) } export function setToken(token) { memoryToken token localStorage.setItem(TOKEN_KEY, token) } export function clearToken() { memoryToken null localStorage.removeItem(TOKEN_KEY) }这套封装最核心的一点是所有业务代码都不要直接操作localStorage而是通过getToken/setToken/clearToken这三个函数。哪天你想把存储方案从localStorage换成sessionStorage或者再加一层加密只需要改这一个文件全项目的调用点都不受影响。这就是我在项目里反复强调的“存储方案隔离”思想。3.3 一个经常被忽略的细节XSS之后怎么办很多前端新手选localStorage的理由是“方便”但忽略了XSS攻击的破坏力。假设你的页面上某个输入框没有做好转义攻击者注入了一段脚本直接执行localStorage.removeItem(token)或者把Token发到第三方服务器用户账号就白送了。这不是危言耸听是我见过真实发生的案例。应对思路是纵深防御第一层永远不要把敏感信息直接用明文渲染进DOM第二层对用户输入做过滤和转义第三层Token本身做短时效设计即使泄露了攻击者拿到的也是一个很快过期的凭证第四层敏感操作记录日志出现异常行为可以追溯。存储选型只是其中一环真正决定安全性的是整个系统的设计。4. 失效处理401拦截、Token刷新与并发请求的坑4.1 为什么“Token过期弹个框”是最差方案有些项目处理Token过期的方案是axios响应拦截器里检测到401直接弹窗“登录已过期”然后清空本地缓存、跳登录页。这个方案粗糙但直观问题在于它完全打断了用户的操作流。设想一个场景用户正在编辑一份很长的表单写到一半去倒水回来顺手点了个保存结果系统把表单页面关了让他重新登录。等登回来刚才填的内容全没了。这种体验不用多来两次用户就流失了。更合理的方向是静默续期Token过期之前或刚过期时前端自动用refresh_token去换一个新的access_token全程用户无感知。只有refresh_token也失效了才强制走重新登录流程。这个思路在移动端App里已经很成熟Web端这几年也越来越普及。4.2 双Token机制与axios拦截器刷新实现双Token机制是目前最主流的静默续期方案登录时后端同时返回两个Token——access_token短期有效比如2小时用于正常业务请求refresh_token长期有效比如7天用于换取新的access_token。业务请求用access_token一旦返回401就带上refresh_token去调刷新接口拿到新的access_token重新发起刚才失败的请求。这里有一个新手几乎必踩的坑多个业务请求同时401时如果每个请求都独自去刷新Token刷新接口会被并发打爆而且旧Token已经被刷新过其他请求再用它去刷新会失败。解决办法是做一个“刷新请求队列”或者“单例刷新”第一个401触发的刷新请求执行期间后续所有的401请求都进入等待队列等刷新完成后再逐个重放。下面这份实现我用了很久核心思路就是“刷新中标记 请求队列”你可以直接参考// refresh or redirect let isRefreshing false let pendingQueue [] function refreshToken() { return axios.post(/auth/refresh, { refreshToken: getRefreshToken() }).then(res { const { accessToken, refreshToken: newRefreshToken } res.data setToken(accessToken) setRefreshToken(newRefreshToken) return accessToken }) } service.interceptors.response.use( (response) response.data, (error) { const { response, config } error if (response response.status 401 !config._retry) { if (getRefreshToken()) { // 如果已经有刷新请求在进行中直接排队等待 if (isRefreshing) { return new Promise((resolve, reject) { pendingQueue.push({ config, resolve, reject }) }) } config._retry true isRefreshing true return refreshToken() .then((newToken) { // 用新Token重放当前请求 config.headers[Authorization] Bearer ${newToken} return service(config) }) .catch((refreshError) { // 刷新失败清缓存跳登录 clearAllToken() router.push(/login) return Promise.reject(refreshError) }) .finally(() { // 刷新完成后重放队列里所有请求 isRefreshing false pendingQueue.forEach(({ config, resolve, reject }) { axios.request(config).then(resolve).catch(reject) }) pendingQueue [] }) } clearAllToken() router.push(/login) } return Promise.reject(error) } )这段代码有几个关键点必须要理解清楚第一config._retry标记防止同一个请求无限次重试第二isRefreshing是一个经典的“分布式锁”思想保证全局只有一次刷新请求第三pendingQueue里存的是等待重放的请求配置不是响应结果所以刷新之后要重新通过axios发出。重放时机放在finally里是为了保证无论刷新成功还是失败队列都能被处理干净不会留下悬挂的Promise。4.3 token exchange failed一类特殊但高频的403/401报错最近网上关于“token exchange failed: token endpoint returned status 403 forbidden”这类报错的讨论特别多很多用IDE插件、命令行工具或者低代码平台的人都会撞上。这里说的其实是OAuth 2.0/OIDC协议里的一类典型错误你用授权码或者refresh_token去Token端点交换访问令牌时身份认证服务器返回了非200状态码其中403代表它“拒绝执行这次交换”。为什么会403常见原因有这么几类一是客户端凭据不匹配比如client_id或者client_secret配置错了二是授权码已经过期或者被使用过OAuth协议里授权码绝大多数是一次性的三是redirect_uri不一致认证服务器要求回调地址必须跟授权请求时完全一样四是某些服务端对IP、地域、用户上下文有策略校验判定当前环境不在允许范围内直接拒绝。如果你在项目里调试这类接口我的建议是按顺序排查先看请求参数里用的授权码或refresh_token是不是最新的再去后台检查应用配置里的client_id和redirect_uri是否跟代码一致最后看服务端日志里有没有具体的拒绝原因字段。这类问题八成是“环境变量配置不一致”比如本地开发环境连的是测试服务端还是生产服务端经常因为一个环境变量指错地方导致403。5. 退出登录不只是清Token那么简单5.1 退出动作前端要做的五件事退出登录从用户视角看就是“点一下退出”但从代码层面看它涉及的状态清理远不止一个Token。我归纳了五个核心动作取消不合法请求。如果有正在pending中的请求退出时应该通过axios的AbortController或者取消令牌把这些请求取消掉避免它们回来后把页面状态又改回去。清空本地Token缓存。access_token、refresh_token都要清别只清一个留一个。我就遇到过只清了access_token、没清refresh_token导致退出后又被静默续期拉回登录态的情况。清理用户信息存储。Vuex/Pinia里的用户资料、权限表、路由表都要重置。忘记清权限路由是很多项目退出后再登录出现页面空白或菜单缺失的第一大原因。重置应用级状态。比如一些全局定时器、WebSocket连接、消息订阅等退出后都应该断开或重置否则用户退出了还在后台收推送。跳转登录页并清理路由历史。用router.replace而不是router.push避免用户按浏览器后退键时又退回到需要鉴权的页面看到一片空白或者闪一下再被弹回登录页。5.2 清除缓存时的“多端”与“多标签页”陷阱这里有一个特别容易被忽视的事多标签页场景下退出登录的状态同步问题。假设用户开了两个标签页操作同一个系统在标签页A点了退出登录标签页B并不会自动感知到。如果这时候用户切到标签页B继续操作系统会用已被清掉的Token去发请求结果就是各种401报错。处理思路有两种。简单方案是依赖storage事件localStorage/sessionStorage在跨标签页写入、修改、删除时其他标签页会收到storage事件我们可以在这个事件里监听Token是否被清空如果是就同步跳登录页。高级方案是用BroadcastChannel跨标签页发消息广播“已退出”事件这样页面能收到明确通知不只是依赖某个存储键的消失。// 跨标签页同步登出 —— storage事件方案 window.addEventListener(storage, (event) { if (event.key access_token !event.newValue) { // 其他标签页清除了Token当前页也退出 clearAllState() router.replace(/login) } })这个写法核心在于每个页面的localStorage存储空间是共享的一个标签页删除了access_token其他标签页会触发storage事件。这时你只需要判断被删除的key是Token并且没有新值写入就能确定“用户在别处退出了”。不过要注意storage事件只在其他标签页触发触发它的那个标签页不会收到自己的事件所以正常退出按钮的处理逻辑里还要额外执行一次自己的清理和跳转。5.3 IDE和命令行工具里的登录退出机制其实也是同一套东西前面热搜里提到了“codex如何退出登录”“codex接deepseek后怎样退出登录”这类问题很多人可能觉得这种AI编程工具跟咱们Web前端的Token处理关系不大。实际上它们底层的登录退出机制和我们在浏览器里处理Token的逻辑惊人地一致工具会缓存一个access_token用于调用后端服务退出登录本质上就是把本机缓存的Token清理掉下次使用再重新走OAuth授权流程。比如某些CLI工具登录后会把凭据写进用户目录下的配置文件里退出登录命令做的就是删除或重置这个配置文件。有些工具还支持“在别的设备上退出登录”比如“wolai怎么退出其他地点登录”就是这个场景本质上就是让服务端把这个Token拉黑或把它的refresh_token作废从而让已泄露的凭证失效。你在自己的Web项目里做退出登录时如果后端已经提供了令牌撤销接口前端能调用就一定要调用——只在前端删掉Token但如果Token被别人复制走了它在到期前仍然是有效的这是个安全隐患。6. 常见问题速查与排查思路6.1 高频问题原因对照表整理一个我在项目里见过最频繁的Token相关问题和排查方向方便你直接对照现象可能原因排查路径登录成功后第一请求仍401Token尚未写入缓存就发请求检查登录回调中setToken与页面请求的先后顺序刷新页面后登录态丢失Token存到了内存/sessionStorage但预期是localStorage核对存储选型与初始化回填逻辑请求偶发401但重新登录就正常多个请求并发401时未做队列Token被重复刷新检查是否有isRefreshing单例锁退出后按浏览器返回仍能进入页面路由守卫只判断Token存在未判断用户状态和路由历史改用router.replace并在守卫里校验完整状态一个标签页退出其他标签页仍可使用缺少跨标签页通知机制监听storage事件或使用BroadcastChanneltoken endpoint返回403 forbiddenclient_id/redirect_uri不匹配、授权码失效、服务端策略拒绝核对应用配置、授权码时效、服务端日志refresh_token为空字符串报400前端传了空串或未正确保存refresh_token检查登录响应解析和刷新接口传参6.2 排查Token问题的三个实用技巧排查Token问题我分享三个百试百灵的实战技巧。第一打开浏览器开发者工具 - Network面板直接看每个请求的Authorization请求头是否正常。这一步可以快速区分问题出在“前端没带Token”“Token过期”还是“服务端拒绝”三层里的哪一层。如果请求头里Token是有的但服务端仍返回401问题大概率在后端校验逻辑前端别瞎折腾。第二在axios拦截器里加一条调试日志。上线前可以去掉但开发环境里把config.url、config.headers.Authorization、response.status打出来能省下大量用console.log一个个点查的时间。// 请求拦截器里加一行 console.log([Request] ${config.method.toUpperCase()} ${config.url}, config.headers.Authorization)第三验证Token解析出来的exp字段。JWT格式的Token是可以用在线工具或者jwt-decode库解开的前端拿到Token后可以自行校验剩余有效期这样在Token真正过期之前就可以触发静默续期从根源上减少“突然401”的概率。6.3 一份可以写进简历的完整链路自检清单把所有这些整合成一份清单既能当项目自检用也能在面试时帮你把“Token处理”这个问题讲得完整、有细节[ ] 登录成功后是否立即保存access_token和refresh_token到安全存储[ ] axios请求拦截器是否统一附加Authorization头[ ] axios响应拦截器是否统一处理401并在必要时静默刷新[ ] 并发401请求是否做了队列与单例刷新控制[ ] Token缓存的存取是否封装在独立模块业务代码无感知[ ] 刷新Token失败时是否清理所有缓存并跳转登录页[ ] 退出登录时是否取消了未完成请求、清理本地存储、重置全局状态[ ] 多标签页环境下是否在cookie/localStorage层面同步了退出状态[ ] 路由守卫是否同时校验Token存在与用户信息完整[ ] 敏感请求是否额外做了二次校验而不是单靠Token清单里每一项背后都对应着实际项目中真实出现过的问题。你能把每条都讲清楚它解决的是什么场景面试官通常就知道你是真正动手处理过而不是背了一堆概念。7. 写在最后的个人经验Token处理这套东西技术难度不高但复杂度都在细节里。我在实际项目中最大的体会是一定要把Token的读写、刷新、清除封装成独立的模块让业务代码离这些细节越远越好。很多问题的根源就是Token逻辑散落在各个页面和组件里今天这里写一个localStorage.setItem明天那里写一个location.reload最后维护起来极其痛苦。最后再分享一个小技巧给Token设置一个“提前刷新时间窗口”。比如access_token有效期是2小时你可以设定在第70分钟时就用refresh_token主动换新Token而不是等到第120分钟接口报401再去补救。结合定时器或者在前端解析JWT里的exp字段来实现这样大部分用户根本不会感知到Token的存在——这才是登录功能最理想的状态让人感觉不到它的存在但它一直在稳定地工作。
返回列表