JSON Web Token(JWT)完全指南

JWT(JSON Web Token)是现代 Web 应用中最广泛使用的身份验证与信息交换标准之一。本文从原理到实践,系统性地介绍 JWT 的方方面面。


一、什么是 JWT?

JWT(JSON Web Token)是一种开放标准(RFC 7519),定义了一种紧凑且自包含的方式,以 JSON 对象的形式在各方之间安全地传输信息。由于数字签名的存在,这些信息是可验证且可信的。

两种核心使用场景:

授权(Authorization):这是 JWT 最常见的用途。用户登录后,每个后续请求都携带 JWT,允许用户访问令牌许可的路由、服务和资源。SSO(Single Sign-On)大量使用 JWT,因为其开销很小,且可跨域使用。

信息交换(Information Exchange):JWT 是在各方之间安全传输信息的好方法。由于可以对 JWT 签名(例如使用公/私钥对),你可以确认发送者的身份。此外,由于签名是用 header 和 payload 计算的,还可以验证内容未被篡改。


二、JWT 的结构

JWT 由三部分组成,用点号(.)分隔:

复制代码
Header.Payload.Signature

一个真实的 JWT 看起来像这样:

复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
```### 2.1 Header(头部)

Header 通常由两部分组成:令牌的类型(`typ`,即 `JWT`)以及所使用的签名算法(`alg`),例如 HMAC SHA256 或 RSA。

```json
{
  "alg": "HS256",
  "typ": "JWT"
}

然后对该 JSON 进行 Base64URL 编码,形成 JWT 的第一部分。

2.2 Payload(载荷)

Payload 包含声明(Claims)。声明是关于实体(通常是用户)及附加数据的陈述。JWT 定义了三种类型的声明:

注册声明(Registered Claims)------预定义的、非强制性但推荐使用的声明集:

字段 全称 说明
iss Issuer 签发方
sub Subject 主题(通常为用户 ID)
aud Audience 接收方
exp Expiration Time 过期时间(Unix 时间戳)
nbf Not Before 生效时间
iat Issued At 签发时间
jti JWT ID JWT 唯一标识,可防重放

公共声明(Public Claims)------由使用者自由定义,但应在 IANA JSON Web Token Registry 中注册或使用带命名空间的 URI,以避免冲突。

私有声明(Private Claims) ------通信双方约定的自定义字段,例如 "role": "admin""permissions": ["read", "write"]

json 复制代码
{
  "sub": "1234567890",
  "name": "Alice",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

重要 :Payload 是经过 Base64URL 编码的,而非加密的。任何持有令牌的人都可以解码并读取内容。绝对不要将密码、敏感个人信息等放入 Payload。

2.3 Signature(签名)

签名用于验证消息在传输过程中未被篡改,对于使用私钥签名的令牌,还可以验证 JWT 的发送者身份。

以 HS256 算法为例,签名的计算方式如下:

复制代码
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

三、JWT 的工作流程整体流程分为两个阶段:认证阶段 (一次性)和授权阶段(每次请求)。

在实际 HTTP 请求中,JWT 通常放在 Authorization 请求头中,格式为:

http 复制代码
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

四、签名算法详解

JWT 支持多种签名算法,选择哪种取决于你的安全需求和架构模式。

4.1 对称算法(HMAC)

使用同一个密钥进行签名和验证。

复制代码
HS256 → HMAC + SHA-256
HS384 → HMAC + SHA-384
HS512 → HMAC + SHA-512

适用场景:签发方与验证方是同一个服务(例如单体应用,或内部微服务之间的信任通信)。

风险:所有需要验证 JWT 的服务都必须持有密钥,一旦泄露,所有令牌都可伪造。

4.2 非对称算法(RSA / ECDSA)

使用私钥签名,公钥验证。

复制代码
RS256 → RSA + SHA-256(最常用)
RS384 / RS512
ES256 → ECDSA + P-256(更小的密钥,更快的计算)
ES384 / ES512

适用场景:Auth Server 持有私钥签发令牌,各 Resource Server 只需公钥即可验证,无需信任链扩散。这是微服务架构中的推荐方案。

复制代码
私钥(Auth Server 独有)→ 签名 JWT
公钥(所有 Resource Server 共享)→ 验证签名

五、实战代码示例

5.1 Node.js(jsonwebtoken)

javascript 复制代码
const jwt = require('jsonwebtoken');

// 签发 JWT
function issueToken(userId, role) {
  const payload = {
    sub: userId,
    role: role,
    iat: Math.floor(Date.now() / 1000),
  };

  return jwt.sign(payload, process.env.JWT_SECRET, {
    algorithm: 'HS256',
    expiresIn: '2h',   // 过期时间
    issuer: 'my-app',
    audience: 'my-api',
  });
}

// 验证 JWT
function verifyToken(token) {
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'],  // 白名单,防止 alg:none 攻击
      issuer: 'my-app',
      audience: 'my-api',
    });
    return { ok: true, payload: decoded };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

// Express 中间件示例
function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }

  const token = authHeader.slice(7);
  const result = verifyToken(token);

  if (!result.ok) {
    return res.status(401).json({ error: result.error });
  }

  req.user = result.payload;  // 注入用户信息
  next();
}

5.2 Python(PyJWT)

python 复制代码
import jwt
import os
from datetime import datetime, timedelta, timezone

SECRET_KEY = os.environ["JWT_SECRET"]
ALGORITHM = "HS256"

# 签发 JWT
def create_token(user_id: str, role: str) -> str:
    payload = {
        "sub": user_id,
        "role": role,
        "iat": datetime.now(timezone.utc),
        "exp": datetime.now(timezone.utc) + timedelta(hours=2),
        "iss": "my-app",
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

# 验证 JWT
def verify_token(token: str) -> dict:
    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=[ALGORITHM],   # 白名单
            issuer="my-app",
        )
        return {"ok": True, "payload": payload}
    except jwt.ExpiredSignatureError:
        return {"ok": False, "error": "Token expired"}
    except jwt.InvalidTokenError as e:
        return {"ok": False, "error": str(e)}

六、安全最佳实践

这是 JWT 开发中最容易出错的地方。以下问题在真实漏洞报告中反复出现。

6.1 算法混淆攻击(alg:none)

危险代码

javascript 复制代码
// ❌ 危险:信任 Header 中的 alg 字段
const decoded = jwt.verify(token, secret);

攻击者可以将 Header 中的 alg 改为 none,然后发送一个没有签名的令牌,部分库会错误地接受它。

正确做法

javascript 复制代码
// ✅ 安全:在验证时显式指定允许的算法白名单
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });

6.2 RS256/HS256 混淆攻击

若服务端使用 RS256,攻击者可以将 alg 改为 HS256,并用服务端的公钥(通常是公开的)作为 HMAC 密钥来签名伪造令牌。始终在验证时硬编码期望的算法,而不是读取令牌 Header 中的算法。

6.3 密钥强度

HMAC 密钥应至少 256 位(32 字节),且随机生成,绝不使用短字符串或常见词组:

bash 复制代码
# 生成强随机密钥
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

6.4 过期时间(exp)

永远要设置过期时间。Access Token 建议 15 分钟到 2 小时,Refresh Token 可以更长(7 到 30 天)。

javascript 复制代码
jwt.sign(payload, secret, { expiresIn: '15m' }); // 短期令牌

6.5 令牌存储

存储位置 XSS 风险 CSRF 风险 推荐度
localStorage 高(JS 可读) ❌ 不推荐
sessionStorage 高(JS 可读) ❌ 不推荐
内存(JS 变量) ✅ 推荐
HttpOnly Cookie 无(JS 不可读) 需配合 CSRF Token ✅ 推荐

6.6 不可撤销性问题与解决方案

JWT 的一大设计特点是无状态------服务端不存储令牌,因此无法主动使其失效。这在以下场景下成为问题:

  • 用户主动登出
  • 账号被封禁
  • 权限变更需立即生效

常用解决方案

复制代码
方案一:短过期时间 + Refresh Token 机制
  Access Token → 15 分钟过期(泄露影响窗口小)
  Refresh Token → 存储在服务端,支持主动吊销

方案二:Token 黑名单(Blocklist)
  Redis 存储已吊销的 jti,验证时检查(牺牲部分无状态性)

方案三:Token 版本号
  用户表中存储 token_version,每次登出自增
  JWT 中携带版本号,验证时与数据库比对

七、JWT vs Session:如何选择?

维度 JWT Session
状态 无状态,服务端不存储 有状态,服务端存储 Session
水平扩展 天然支持,无需共享存储 需 Session 共享(Redis 等)
令牌吊销 困难(需额外机制) 简单(直接删除 Session)
跨域/跨服务 天然支持(Authorization Header) 受 Cookie 同源限制
网络开销 每次请求携带令牌(较大) Cookie 较小
微服务/SSO 非常适合 较难实现
即时权限变更 需要额外设计 立即生效

选择建议:微服务架构、需要 SSO(Single Sign-On)的场景、无状态 API,倾向 JWT;传统 Web 应用、需要立即吊销会话、更严格的安全控制,倾向 Session 或两者结合(使用 JWT 作为 Refresh Token 的凭证,Session 作为短期授权)。


八、Refresh Token 模式

生产环境中,通常结合使用短期 Access Token 和长期 Refresh Token:

复制代码
登录成功
  ↓
返回:
  access_token(JWT,15 分钟过期)
  refresh_token(不透明随机字符串,30 天,存储在 HttpOnly Cookie)

访问资源:携带 access_token
  ↓ 若 access_token 过期(401)
  ↓
用 refresh_token 换取新 access_token(POST /auth/refresh)
  → Auth Server 验证 refresh_token(查库)
  → 若有效,颁发新 access_token(可选:Rotation,同时更新 refresh_token)
  ↓
继续使用新 access_token

Refresh Token Rotation(轮换)是一种重要的安全机制:每次使用 Refresh Token 时,同时使旧令牌失效并颁发新令牌,若检测到旧令牌被重复使用,则认为令牌已被盗,立即吊销所有相关令牌。


九、常见误区总结

误区一:JWT 是加密的。 JWT 默认只做签名(JWS),不加密。Payload 是 Base64URL 编码,任何人都可以解码读取。若需加密,使用 JWE(JSON Web Encryption)。

误区二:JWT 比 Session 更安全。 JWT 和 Session 各有优劣,安全性取决于正确实现,不是 JWT 本身更安全。

误区三:在 JWT 中存储密码或敏感数据。 绝不这样做。

误区四:JWT 可以无限使用。 一定要设置合理的过期时间,并考虑令牌吊销机制。

误区五:alg: none 被某些库接受是小事。 这是一个严重的安全漏洞,一定要在验证时白名单指定算法。


JWT 凭借其无状态、自包含、跨语言的特性,成为了现代 API 认证的事实标准。但使用得当的关键在于理解它的设计边界:JWT 解决了"验证信息的真实性与完整性"的问题,而"令牌吊销"、"存储安全"等问题需要在架构层面补充解决。掌握这些,才能将 JWT 用好、用安全。

相关推荐
IT_陈寒1 小时前
Python闭包里藏的这个坑,差点让我加班到凌晨
前端·人工智能·后端
IT_陈寒1 小时前
Java注解空指针?这个坑我踩得莫名其妙
前端·人工智能·后端
JAVA社区1 小时前
Java高级全套教程(十一)—— Kubernetes 超详细企业级实战详解
java·运维·微服务·容器·面试·kubernetes
H0r1zon.2 小时前
PinCopy:双击 Ctrl,把剪贴板「钉」在屏幕上
前端
kyriewen2 小时前
大厂面试新规:不会用AI编程,直接挂
前端·面试·ai编程
努力找实习的前端小白2 小时前
useImperativeHandle,useRef,forwardRef的协作关系
前端·面试
ZengLiangYi2 小时前
Fastify 加 Electron:把 Web 服务嵌进桌面应用
前端·javascript·后端
qq_2518364573 小时前
基于nodejs express +vue 天天商城系统设计与实现 (源码 文档)
前端·vue.js·express
在繁华处3 小时前
Java从零到熟练(九):并发编程基础
java·开发语言