如何在HTMX中处理JWT令牌刷新?
完全在服务器端处理HTMX中JWT刷新的并发问题
当然可以完全在服务器端解决这个并发请求导致的刷新令牌失效问题,核心是避免同一旧刷新令牌被多次触发令牌刷新,同时给并发请求提供有效的新令牌。下面是两种落地性强的方案:
方案一:刷新令牌复用窗口 + 关联跟踪
给数据库中的刷新令牌记录增加两个字段:
last_used_at:记录该刷新令牌最后被用来换取新令牌的时间replaced_by_token:记录替换它的新刷新令牌ID
处理逻辑如下:
- 当收到携带过期访问令牌的请求时,提取刷新令牌并验证其有效性
- 若刷新令牌有效,先检查它是否已经被用来生成过新令牌(即
replaced_by_token不为空,且last_used_at在最近5-10秒内)- 如果是,直接返回对应的新访问令牌,不再生成新的刷新令牌
- 如果不是,生成新的访问+刷新令牌,更新旧令牌的
last_used_at和replaced_by_token,同时将旧令牌的失效时间延迟5-10秒(而非立刻标记为无效)
- 后续并发请求携带同一旧刷新令牌时,会直接获取到刚生成的新访问令牌,无需重复刷新
这种方式既解决了并发问题,又不会过度延长旧令牌的有效期,安全性可控。
方案二:分布式锁控制刷新流程
如果你的服务是多实例部署,需要用分布式锁(比如Redis锁)来保证同一刷新令牌同一时间只有一个请求能触发令牌刷新:
- 当需要处理刷新令牌时,先针对该刷新令牌获取分布式锁(锁的有效期设为5秒左右)
- 拿到锁的请求执行令牌刷新逻辑:生成新令牌、更新数据库中旧令牌状态
- 未拿到锁的请求等待锁释放后,重新查询该刷新令牌的状态,发现已经被替换,直接返回新的访问令牌
伪代码示例(Express中间件)
async function htmxAuthMiddleware(req, res, next) { const accessToken = req.headers.authorization?.split(' ')[1]; const refreshToken = req.cookies.refreshToken; // 验证访问令牌 try { const payload = verifyAccessToken(accessToken); req.user = payload; return next(); } catch (err) { if (!refreshToken) return res.sendStatus(401); // 获取分布式锁 const lockKey = `refresh_lock:${refreshToken}`; const lock = await redisClient.set(lockKey, 'locked', { EX: 5, NX: true }); if (!lock) { // 未拿到锁,等待100ms后重试 await new Promise(resolve => setTimeout(resolve, 100)); return htmxAuthMiddleware(req, res, next); } try { // 再次验证刷新令牌有效性 const storedToken = await db.refreshTokens.findOne({ token: refreshToken }); if (!storedToken || storedToken.isRevoked) { return res.sendStatus(401); } // 检查是否已有新令牌生成 if (storedToken.replacedByToken) { const newAccessToken = generateAccessToken(storedToken.userId); // 通过HTMX响应头更新客户端的Authorization头 res.set('HX-Headers', JSON.stringify({ Authorization: `Bearer ${newAccessToken}` })); req.user = { userId: storedToken.userId }; return next(); } // 生成新令牌 const newAccessToken = generateAccessToken(storedToken.userId); const newRefreshToken = generateRefreshToken(); // 更新数据库 await db.refreshTokens.updateOne( { token: refreshToken }, { $set: { replacedByToken: newRefreshToken, lastUsedAt: new Date() } } ); await db.refreshTokens.insertOne({ token: newRefreshToken, userId: storedToken.userId, expiresAt: new Date(Date.now() + 7*24*3600*1000) }); // 更新客户端的刷新令牌Cookie和请求头 res.cookie('refreshToken', newRefreshToken, { httpOnly: true, secure: true }); res.set('HX-Headers', JSON.stringify({ Authorization: `Bearer ${newAccessToken}` })); req.user = { userId: storedToken.userId }; next(); } finally { // 释放锁 await redisClient.del(lockKey); } } }
配合HTMX的优化点
利用HTMX的HX-Headers响应头,服务器可以直接更新客户端后续请求的Authorization头,无需客户端编写任何额外的重试逻辑,完全实现服务器端的自动身份续期。
内容的提问来源于stack exchange,提问作者PortalGamesMais
相关产品推荐
相关产品推荐

