基于OAuth2的前后端分离Web应用刷新Access Token方案问询
安全高效刷新Access Token的方案(前后端分离OAuth2场景)
Hey,针对你这个前后端分离的OAuth2架构,我来分享一套经过实践验证的安全高效的刷新流程,刚好匹配你后端存Refresh Token、前端存Access Token的设定:
一、核心刷新流程
这是最基础的链路逻辑,先把流程跑通:
- 前端每次发起业务请求时,在请求头里携带
Authorization: Bearer ${accessToken} - 后端校验Access Token:
- 令牌有效就正常返回业务数据
- 如果返回
401 Unauthorized,且明确标记是Access Token过期(一定要和其他401场景区分开,比如令牌伪造、权限不足),前端触发刷新流程
- 前端调用后端专门的刷新令牌接口(比如
POST /oauth/token),请求体携带:grant_type=refresh_tokenrefresh_token=${本地存储的Refresh Token}- (如果你的Client是保密型,可能还要加
client_id和client_secret,但前端作为公开Client的话,后端要做好来源校验)
- 后端校验Refresh Token:
- 确认有效且未过期,就签发新的Access Token(建议同时签发新的Refresh Token,进一步提升安全性)
- 更新后端存储的Refresh Token(如果发了新的),作废旧令牌
- 将新的Access Token(和新Refresh Token)返回给前端
- 前端更新本地存储的令牌,重新发起之前失败的业务请求
二、优化体验与效率的细节
1. 提前刷新,实现无感体验
别等Access Token完全过期才动手!前端可以从JWT的exp字段解析出过期时间,每次请求前检查剩余有效期,如果小于设定的阈值(比如5分钟),就提前调用刷新接口。用户完全不会感知到这个过程,体验更流畅。
2. 解决并发请求的竞态问题
如果同时有多个请求因为令牌过期返回401,别让每个请求都触发刷新——这会导致重复调用刷新接口,浪费资源。可以用全局Promise锁来处理:
- 第一次触发刷新时,把刷新请求的Promise存在全局变量里
- 后续所有失败的请求都等待这个Promise完成,拿到新令牌后再统一重发
- 刷新完成后清空全局变量,避免影响后续请求
给你一段前端伪代码参考:
// 全局变量存储刷新中的Promise let refreshInProgress = null; async function fetchWithAuth(url, options = {}) { const accessToken = localStorage.getItem('accessToken'); try { const response = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${accessToken}` } }); // 判定为Access Token过期的401(后端需自定义响应头标记) if (response.status === 401 && response.headers.get('X-Token-Expired') === 'true') { if (!refreshInProgress) { refreshInProgress = refreshAccessToken(); } // 等待刷新完成 const newTokens = await refreshInProgress; refreshInProgress = null; // 更新本地令牌 localStorage.setItem('accessToken', newTokens.accessToken); localStorage.setItem('refreshToken', newTokens.refreshToken); // 重发原请求 return fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${newTokens.accessToken}` } }); } return response; } catch (err) { // 异常时清空全局锁 refreshInProgress = null; throw err; } } async function refreshAccessToken() { const refreshToken = localStorage.getItem('refreshToken'); const response = await fetch('/oauth/token', { method: 'POST', body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }) }); if (!response.ok) { // 刷新失败,比如Refresh Token过期,直接跳登录 window.location.href = '/login'; throw new Error('Refresh token invalid or expired'); } return response.json(); }
三、安全层面的关键注意事项
安全是OAuth2的核心,这些细节不能少:
- Refresh Token的存储:
- 别把Refresh Token存在
localStorage!建议存在HttpOnly、Secure、SameSite=Strict的Cookie里(如果前后端同域或配置了跨域Cookie),能有效避免XSS攻击窃取令牌 - 如果跨域场景没法用Cookie,一定要对Refresh Token加密后再存在前端存储,同时页面要做好XSS防护
- 别把Refresh Token存在
- 后端校验逻辑:
- 除了校验Refresh Token的签名和过期时间,还要检查它是否在后端的存储列表中(防止篡改或复用)
- 每次刷新后,一定要签发新的Refresh Token并作废旧的,减少令牌泄露后的风险
- 给Refresh Token设置合理的过期时间,并且支持用户登出时手动注销令牌
- 接口防护:
- 刷新令牌接口要做限流,防止暴力破解
- 前端作为公开Client,后端要验证请求的Origin/Referer,或者结合其他方式校验请求合法性
四、异常处理预案
- 如果Refresh Token也过期了,前端直接跳转到登录页面,引导用户重新认证
- 如果刷新接口返回401(比如Refresh Token被篡改、已注销),同样触发登录流程
- 捕获刷新过程中的网络异常,给用户友好提示(比如“登录状态失效,请重新登录”),别让用户摸不着头脑
内容的提问来源于stack exchange,提问作者patak
相关产品推荐
相关产品推荐

