如何通过REST API使用JWT保持用户登录状态
使用JWT实现REST API用户持久登录的无卡顿方案
要解决JWT过期导致的客户端卡顿问题,核心思路是提前刷新令牌,而非等令牌过期后再处理。以下是具体实现方案:
核心逻辑
- 区分两类令牌:Access Token(短有效期,如15分钟)用于日常API请求,Refresh Token(长有效期,如7天)用于刷新Access Token。
- 客户端每次发起API请求前,检查Access Token是否即将过期(比如提前1-2分钟),若即将过期则静默调用刷新接口获取新的Access Token,再用新令牌发起原请求。
- 将刷新逻辑封装为请求拦截器,自动处理令牌更新,业务代码无需感知此过程。
客户端实现(以Axios为例)
通过拦截器封装刷新逻辑,同时处理并发请求的锁机制,避免重复刷新:
// 全局标记是否正在刷新令牌 window.isRefreshing = false; // 存储等待刷新完成后执行的请求队列 window.requestQueue = []; // Axios请求拦截器 axios.interceptors.request.use(async (config) => { const accessToken = localStorage.getItem('accessToken'); const refreshToken = localStorage.getItem('refreshToken'); if (!accessToken) { // 无有效令牌,跳转登录页 window.location.href = '/login'; return Promise.reject(new Error('无访问令牌')); } // 解码JWT获取过期时间(exp为秒级时间戳) const decodedPayload = JSON.parse(atob(accessToken.split('.')[1])); const now = Date.now() / 1000; const timeUntilExpire = decodedPayload.exp - now; // 提前1分钟刷新令牌 if (timeUntilExpire < 60 && refreshToken) { if (!window.isRefreshing) { window.isRefreshing = true; try { // 调用刷新令牌接口 const refreshRes = await axios.post('/api/refresh-token', { refreshToken }); const newAccessToken = refreshRes.data.accessToken; const newRefreshToken = refreshRes.data.refreshToken; // 更新本地存储的令牌 localStorage.setItem('accessToken', newAccessToken); localStorage.setItem('refreshToken', newRefreshToken); // 更新当前请求的Authorization头 config.headers.Authorization = `Bearer ${newAccessToken}`; // 执行队列中等待的请求 window.requestQueue.forEach(callback => callback(newAccessToken)); window.requestQueue = []; } catch (err) { // 刷新失败,清空令牌并跳转登录 localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); window.location.href = '/login'; return Promise.reject(err); } finally { window.isRefreshing = false; } } else { // 正在刷新,将当前请求加入队列等待 return new Promise((resolve) => { window.requestQueue.push((newToken) => { config.headers.Authorization = `Bearer ${newToken}`; resolve(config); }); }); } } else { // 令牌有效,直接设置Authorization头 config.headers.Authorization = `Bearer ${accessToken}`; } return config; });
服务端实现(以Node.js/Express为例)
提供专门的刷新令牌接口,验证Refresh Token有效性并生成新的Access Token(可选旋转Refresh Token):
const jwt = require('jsonwebtoken'); const User = require('./models/User'); // 刷新令牌接口 app.post('/api/refresh-token', async (req, res) => { const { refreshToken } = req.body; if (!refreshToken) { return res.status(401).json({ message: '缺少Refresh Token' }); } try { // 验证Refresh Token签名和有效期 const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET); // 查询数据库验证Refresh Token是否属于当前用户(防止令牌被复用) const user = await User.findById(decoded.userId); if (!user || user.refreshToken !== refreshToken) { return res.status(403).json({ message: '无效的Refresh Token' }); } // 生成新的Access Token(15分钟有效期) const newAccessToken = jwt.sign( { userId: user._id, username: user.username }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' } ); // 可选:旋转Refresh Token,生成新的并更新数据库(提升安全性) const newRefreshToken = jwt.sign( { userId: user._id }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' } ); user.refreshToken = newRefreshToken; await user.save(); // 返回新令牌 res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken }); } catch (err) { // Refresh Token过期或无效 return res.status(403).json({ message: 'Refresh Token已过期或无效' }); } });
关键注意事项
- Refresh Token存储安全:同域应用建议将Refresh Token存放在HttpOnly、Secure的Cookie中,避免XSS攻击;跨域场景存储在localStorage时,需做好XSS防护。
- 强制启用HTTPS:所有令牌相关请求必须通过HTTPS传输,防止令牌被窃听。
- Refresh Token失效机制:用户主动登出时,需将对应的Refresh Token从数据库删除或加入黑名单;检测到令牌泄露时,也要及时失效处理。
- 避免敏感信息:不要将用户密码、银行卡号等敏感数据放入JWT的Payload中,因为JWT是Base64编码可解码的。
- 平衡过期时间:Access Token设短有效期(15-30分钟),Refresh Token设长有效期(7-30天),在安全性和用户体验间找平衡。
内容的提问来源于stack exchange,提问作者Adjetey Octavius
相关产品推荐
相关产品推荐

