jwtsecurityauthentication

2026年JWT安全最佳实践

· Cosyslabs

JWT安全漏洞始终位居API漏洞榜首。使用RS256或ES256算法,明确拒绝 none 算法,将访问令牌有效期设置为15分钟以内,实施刷新令牌轮换,并在信任任何声明之前始终在服务器端验证签名。

什么是JWT?

JSON Web Token由三个Base64URL编码的段组成,以点分隔:

头部.载荷.签名
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature
  • 头部:算法和令牌类型
  • 载荷:声明(用户数据、过期时间等)
  • 签名:令牌未被篡改的加密证明

签名是您必须验证的内容。没有经过验证的签名的JWT只是未经验证的JSON。

关键漏洞:算法混淆

none 算法攻击

早期的JWT库接受头部中的 alg: "none",意味着不需要签名。攻击者可以伪造任何载荷:

{
  "alg": "none",
  "typ": "JWT"
}

没有签名检查,他们可以声称是任何用户。修复方法:

// Node.js — 始终明确指定允许的算法
jwt.verify(token, publicKey, { algorithms: ["RS256"] });

// 永远不允许 "none"
// 永远不要使用 algorithms: ["RS256", "none"] — 这是一个漏洞

RS256 vs HS256 混淆攻击

HS256(HMAC)使用共享密钥——同一个密钥用于签名和验证。RS256(RSA)使用密钥对——私钥签名,公钥验证。

攻击:如果一个库看到 alg: "HS256" 并将RS256公钥用作HMAC密钥,获得了公钥(它是公开的!)的攻击者就可以伪造令牌。

始终在服务器端固定算法。永远不要从令牌中读取 alg 来决定如何验证它。

// 有漏洞 — 从令牌读取 alg
function verify(token, key) {
  const { alg } = decodeHeader(token);
  return verifyWith(token, key, alg); // 永远不要这样做
}

// 安全 — 算法在服务器端硬编码
function verify(token) {
  return jwt.verify(token, PUBLIC_KEY, { algorithms: ["RS256"] });
}

算法建议

算法类型使用场景
ES256非对称(ECDSA)新项目的最佳选择——签名小、速度快
RS256非对称(RSA)广泛支持,适合互操作性
HS256对称(HMAC)仅当密钥真正共享且从未暴露时
none永远不要使用

对于多个服务验证令牌的公共API,使用非对称算法(ES256/RS256)。私钥保留在认证服务器上;所有其他服务只持有公钥。

令牌过期和刷新策略

短期访问令牌限制了令牌被盗的损害。使用双令牌模式:

  • 访问令牌:5–15分钟后过期,随每个API请求发送
  • 刷新令牌:7–30天后过期,安全存储,仅用于获取新的访问令牌
// 颁发令牌
const accessToken = jwt.sign(
  { sub: user.id, role: user.role },
  PRIVATE_KEY,
  { algorithm: "ES256", expiresIn: "15m" }
);

const refreshToken = jwt.sign(
  { sub: user.id, jti: crypto.randomUUID() },
  REFRESH_SECRET,
  { expiresIn: "7d" }
);

刷新令牌轮换

每次使用刷新令牌时,使其失效并颁发新令牌。如果检测到被盗的刷新令牌被使用了两次,则使整个会话失效:

async function refreshTokens(oldRefreshToken) {
  const payload = jwt.verify(oldRefreshToken, REFRESH_SECRET);
  
  // 检查令牌之前是否已使用(重用检测)
  const tokenRecord = await db.refreshTokens.findOne({ jti: payload.jti });
  
  if (!tokenRecord || tokenRecord.used) {
    // 检测到令牌重用 — 撤销整个家族
    await db.refreshTokens.revokeFamily(payload.sub);
    throw new Error("检测到令牌重用");
  }
  
  // 标记为已使用
  await db.refreshTokens.markUsed(payload.jti);
  
  // 颁发新的令牌对
  return issueTokenPair(payload.sub);
}

安全存储JWT

存储位置XSS风险CSRF风险建议
localStorage永远不用于认证令牌
sessionStorage永远不用于认证令牌
内存(JS变量)适合访问令牌
HttpOnly Cookie最适合刷新令牌 + CSRF令牌

将访问令牌存储在JavaScript内存中。将刷新令牌存储在 HttpOnlySecureSameSite=Strict Cookie中。

// 将刷新令牌设置为HttpOnly Cookie
res.cookie("refresh_token", refreshToken, {
  httpOnly: true,
  secure: true,
  sameSite: "strict",
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7天
  path: "/auth/refresh", // 仅发送到刷新端点
});

始终验证的声明

除了验证签名外,还要验证这些标准声明:

jwt.verify(token, PUBLIC_KEY, {
  algorithms: ["ES256"],
  issuer: "https://auth.yourdomain.com",    // iss 声明
  audience: "https://api.yourdomain.com",   // aud 声明
  // exp 自动检查
  // nbf 自动检查
});
声明含义始终验证
exp过期时间
nbf生效时间
iss颁发者
aud受众
sub主体(用户ID)是,与会话匹配
jtiJWT ID是,用于撤销

JWT撤销

JWT是无状态的——有效令牌在过期前保持有效。对于即时撤销(注销、密码更改、账户暂停),维护一个阻止列表:

// 注销时
await redis.setex(`revoked:${payload.jti}`, tokenTtlSeconds, "1");

// 每次请求时
async function verifyToken(token) {
  const payload = jwt.verify(token, PUBLIC_KEY, { algorithms: ["ES256"] });
  
  const isRevoked = await redis.exists(`revoked:${payload.jti}`);
  if (isRevoked) throw new Error("令牌已撤销");
  
  return payload;
}

调试JWT

使用JWT解码工具检查头部和载荷,无需将令牌发送到外部服务。所有解码都在您的浏览器中进行。

安全检查清单

  • 使用ES256或RS256——公共API永远不要用HS256
  • 在库配置中明确拒绝 alg: "none"
  • 在服务器端固定算法——永远不要从令牌头部读取
  • 将访问令牌有效期设置为5–15分钟
  • 实施带有重用检测的刷新令牌轮换
  • 将刷新令牌存储在HttpOnly Cookie中,访问令牌存储在内存中
  • 在每次请求时验证 issaudexpnbf
  • 实施基于JTI的撤销,用于注销/密码更改
  • 永远不要记录完整的JWT——它们是持有者凭据

更多Cosyslabs工具