Strapi(v4.x)问题:因无法获取用户ID导致过期JWT令牌刷新失败
JWT令牌过期后无法刷新的问题解决
问题背景
用户JWT令牌过期时,验证流程直接失败,无法从中提取用户ID;但生成新JWT令牌必须依赖用户ID,陷入无法刷新的死循环。
当前刷新令牌的实现代码如下:
plugin.controllers.auth.refreshToken = async (ctx) => { const params = _.assign(ctx.request.body); try { const { id } = await strapi.plugins[ "users-permissions" ].services.jwt.verify(params.jwt); // 令牌过期时无法获取用户ID const RAW_SQL = `SELECT id FROM up_users WHERE id = ${id}`; const entriesResult = await strapi.db.connection.raw(RAW_SQL); const rows = entriesResult.rows; if (_.size(rows) === 1) { const user = _.first(rows); ctx.send({ jwt: strapi.plugins["users-permissions"].services.jwt.issue({ id: user.id, }), }); } } catch (e) { return ctx.badRequest(null, "Invalid token"); } };
解决方案
方法1:忽略过期验证解码JWT
Strapi的JWT服务基于jsonwebtoken库,可直接调用decode方法跳过过期验证提取用户ID,再验证用户合法性:
const jwt = require('jsonwebtoken'); plugin.controllers.auth.refreshToken = async (ctx) => { const params = _.assign(ctx.request.body); try { // 直接解码令牌,不校验过期时间 const decoded = jwt.decode(params.jwt); if (!decoded?.id) { return ctx.badRequest(null, "Invalid token"); } // 查询用户是否存在 const user = await strapi.db.query('plugin::users-permissions.user').findOne({ where: { id: decoded.id } }); if (user) { ctx.send({ jwt: strapi.plugins["users-permissions"].services.jwt.issue({ id: user.id, }), }); } else { return ctx.badRequest(null, "User not found"); } } catch (e) { return ctx.badRequest(null, "Invalid token"); } };
注意:此方法仅适用于短期过期的令牌,若令牌过期过久建议拒绝刷新,降低安全风险。
方法2:引入独立刷新令牌机制(推荐)
更安全的方案是使用分离的刷新令牌(Refresh Token):
- 用户登录时,同时返回短期有效期的JWT访问令牌和长期有效期的刷新令牌,后者存储在数据库
- 刷新时客户端提交刷新令牌,服务端验证令牌的有效性(存在性、未过期),再签发新的访问令牌
- 支持刷新令牌的失效机制(如单次使用即失效、定期滚动更新)
实现示例
- 在用户表添加
refresh_token和refresh_token_expires_at字段 - 登录接口返回双令牌:
plugin.controllers.auth.login = async (ctx) => { // ... 原有登录逻辑,获取user对象 ... const accessToken = strapi.plugins["users-permissions"].services.jwt.issue({ id: user.id }); const refreshToken = jwt.sign({ id: user.id }, strapi.config.get('plugin.users-permissions.jwt.secret'), { expiresIn: '7d' }); // 更新用户的刷新令牌及过期时间 await strapi.db.query('plugin::users-permissions.user').update({ where: { id: user.id }, data: { refresh_token: refreshToken, refresh_token_expires_at: new Date(Date.now() + 7*24*60*60*1000) } }); ctx.send({ jwt: accessToken, refreshToken: refreshToken }); };
- 刷新令牌接口验证刷新令牌:
plugin.controllers.auth.refreshToken = async (ctx) => { const { refreshToken } = ctx.request.body; try { // 验证刷新令牌签名 const decoded = jwt.verify(refreshToken, strapi.config.get('plugin.users-permissions.jwt.secret')); if (!decoded?.id) { return ctx.badRequest(null, "Invalid refresh token"); } // 查询用户,验证刷新令牌匹配且未过期 const user = await strapi.db.query('plugin::users-permissions.user').findOne({ where: { id: decoded.id, refresh_token: refreshToken, refresh_token_expires_at: { $gt: new Date() } } }); if (user) { // 签发新访问令牌 const newAccessToken = strapi.plugins["users-permissions"].services.jwt.issue({ id: user.id }); // 可选:滚动更新刷新令牌 const newRefreshToken = jwt.sign({ id: user.id }, strapi.config.get('plugin.users-permissions.jwt.secret'), { expiresIn: '7d' }); await strapi.db.query('plugin::users-permissions.user').update({ where: { id: user.id }, data: { refresh_token: newRefreshToken, refresh_token_expires_at: new Date(Date.now() + 7*24*60*60*1000) } }); ctx.send({ jwt: newAccessToken, refreshToken: newRefreshToken }); } else { return ctx.badRequest(null, "Invalid or expired refresh token"); } } catch (e) { return ctx.badRequest(null, "Invalid refresh token"); } };
方法3:修改Strapi JWT验证参数
若Strapi的JWT服务支持传递验证选项,可在调用verify时添加ignoreExpiration: true跳过过期校验:
const { id } = await strapi.plugins["users-permissions"].services.jwt.verify(params.jwt, { ignoreExpiration: true });
若Strapi的封装不支持该参数,直接使用方法1的解码方式即可。
内容的提问来源于stack exchange,提问作者Chris Michael
相关产品推荐
相关产品推荐

