You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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访问令牌和长期有效期的刷新令牌,后者存储在数据库
  • 刷新时客户端提交刷新令牌,服务端验证令牌的有效性(存在性、未过期),再签发新的访问令牌
  • 支持刷新令牌的失效机制(如单次使用即失效、定期滚动更新)

实现示例

  1. 在用户表添加refresh_token和refresh_token_expires_at字段
  2. 登录接口返回双令牌:
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
  });
};
  1. 刷新令牌接口验证刷新令牌:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 19:33:31