JWT 完全指南:从结构到 RS256 与 Refresh Token 的生产实战

JWT(JSON Web Token)是当代 API 鉴权的事实标准。微服务之间互信、单点登录、API Key、OAuth2 的 access_token —— 全是 JWT 或其变种。它的"无状态、自包含"特性让分布式鉴权变得简单。但 JWT 也是被误用最严重的技术之一,"把 JWT 当 Session 用" / "用 none 算法" / "jwt 永不过期" 这类反模式比比皆是。这篇文章把 JWT 讲透,涵盖结构、签名、安全坑、生产实践。

JWT 的结构

一个 JWT 长这样,用 . 分成三段:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJtb3JlcyJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

# 三段:
# Header.Payload.Signature

# Header(base64url 解码)
{
  "alg": "HS256",
  "typ": "JWT"
}

# Payload(base64url 解码)—— 注意这是公开可读的!
{
  "sub": "12345",        # subject,通常是用户 ID
  "name": "mores",
  "iat": 1700000000,     # issued at
  "exp": 1700003600      # expiration
}

# Signature
HMAC_SHA256(
  base64url(Header) + "." + base64url(Payload),
  secret_key
)

核心特点

  • 自包含:用户信息在 token 里,服务端无需查库。
  • 可验证:签名保证内容没被篡改。
  • 无状态:服务端不存 token,扩容简单。
  • 可读不可改:Payload 是 base64,任何人能解码看内容;但改了签名验证失败。

标准 Claims

RFC 7519 定义的标准字段:

iss  Issuer        颁发者(如 "https://auth.myapp.com")
sub  Subject       主题(通常用户 ID)
aud  Audience      接收方(API 服务 ID,验证用)
exp  Expiration    过期时间(unix timestamp)
iat  Issued At     签发时间
nbf  Not Before    生效时间(可以让 token "未来才有效")
jti  JWT ID        唯一 ID,可用于撤销

# 自定义 claim 加在上面:
{
  "iss": "https://auth.myapp.com",
  "sub": "user_123",
  "aud": "https://api.myapp.com",
  "exp": 1700003600,
  "role": "admin",
  "tenant_id": "company_42"
}

签名算法

HS256(对称)

HMAC + SHA256,用一个 secret 既签名又验证。简单,但验证方需要知道 secret —— 不适合多方场景。

// 签
const token = jwt.sign({sub: 'u1'}, SECRET, { algorithm: 'HS256' });
// 验
const decoded = jwt.verify(token, SECRET, { algorithms: ['HS256'] });

RS256(非对称,推荐)

RSA + SHA256,用私钥签名,公钥验证。颁发方独占私钥,所有验证方持公钥。多服务场景的首选。

const token = jwt.sign({sub: 'u1'}, PRIVATE_KEY, { algorithm: 'RS256' });
const decoded = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });

ES256(椭圆曲线,现代)

ECDSA + SHA256,功能同 RS256 但密钥更短(P-256),签名更快。新项目优先 ES256。

禁用 none 算法

JWT 标准里有个 "alg": "none"(不签名)选项。某些库默认接受 —— 攻击者构造 none 的 token 直接通过验证!验证时必须显式指定允许的算法:

# 危险
jwt.decode(token);                       # 不验证,只解析
jwt.verify(token, secret);                # 某些老版本可能接受 none

# 安全
jwt.verify(token, secret, { algorithms: ['RS256'] });   # 显式指定算法

JWT 的常见误用

误用 1:用 JWT 当 Session

JWT 无状态意味着颁发后没法立刻撤销。用户登出 / 改密码 / 被封号后,旧 token 在过期前仍然有效。如果 token 过期时间设 7 天,被盗号后这 7 天都防不住。

正确用法:

  • access_token 短期(15 分钟 - 2 小时),配 refresh_token。
  • 关键场景查黑名单:登出 / 改密码时把 jti 加入 Redis 黑名单,验证时查一下。
  • 真的需要可控的会话:用传统 Session(存 Redis),把 session_id 给客户端。

误用 2:把敏感数据放 Payload

Payload 是 base64,谁都能解码看。不要放密码、信用卡号、隐私数据。只放鉴权必要的标识(用户 ID、role、tenant_id)。

误用 3:token 永不过期

"为了用户体验,token 设 1 年" —— token 泄漏了 1 年都防不住。规则:access_token ≤ 1 小时,refresh_token 短到 30 天。

误用 4:secret 太弱

HS256 的 secret 是字符串,如果短或简单,可以被暴力破解(JWT cracker 工具几分钟搞定常见弱密码)。secret 至少 256 位随机:

openssl rand -base64 32

Access Token + Refresh Token 模式

登录返回:
{
  "access_token": "eyJ...",     // 15 分钟过期,带在每个 API 请求
  "refresh_token": "eyJ...",    // 30 天,只用来换新 access_token
}

# 客户端
- access_token 放内存(JS 变量),不放 localStorage(防 XSS)
- refresh_token 放 HttpOnly Cookie(防 XSS)
- 收到 401 时,用 refresh_token 调 /auth/refresh 拿新 access_token,再重试请求

# refresh_token rotation(强烈推荐)
每次用 refresh_token 换新 access_token 时,同时换一个新 refresh_token
旧 refresh_token 立刻失效。攻击者拿到旧的也没用,
而且如果旧的被使用过,说明可能被盗 -> 触发安全告警。

JWKS:动态获取公钥

RS256 签发的 JWT,验证方怎么知道用哪个公钥?用 JWKS(JSON Web Key Set):

# 颁发者公开 .well-known/jwks.json:
GET https://auth.myapp.com/.well-known/jwks.json
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key1",           // 密钥 ID
      "use": "sig",
      "n": "...", "e": "AQAB"   // 公钥参数
    },
    {
      "kty": "RSA",
      "kid": "key2",            // 新密钥,用于密钥轮换
      ...
    }
  ]
}

# JWT 的 Header 带 kid,告诉验证方用哪个公钥
{ "alg": "RS256", "typ": "JWT", "kid": "key1" }

# 验证流程:
1. 取 token 的 kid
2. 从 JWKS 找对应公钥(可缓存)
3. 用该公钥验证签名

这套机制让颁发方可以定期轮换密钥(发布新 kid,新 token 用新 kid 签,旧 token 仍能用旧 kid 验,直到过期 + 公钥下线)。

实战:Node.js 实现

const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('./keys/private.pem');
const publicKey = fs.readFileSync('./keys/public.pem');

// 签发(登录成功后)
function issueTokens(user) {
    const accessToken = jwt.sign(
        { sub: user.id, role: user.role },
        privateKey,
        {
            algorithm: 'RS256',
            issuer: 'https://auth.myapp.com',
            audience: 'https://api.myapp.com',
            expiresIn: '15m',
        }
    );
    const refreshToken = jwt.sign(
        { sub: user.id, jti: crypto.randomUUID() },
        privateKey,
        { algorithm: 'RS256', expiresIn: '30d' }
    );
    return { accessToken, refreshToken };
}

// 验证中间件
function authMiddleware(req, res, next) {
    const token = req.headers.authorization?.replace('Bearer ', '');
    if (!token) return res.status(401).end();
    try {
        const payload = jwt.verify(token, publicKey, {
            algorithms: ['RS256'],
            issuer: 'https://auth.myapp.com',
            audience: 'https://api.myapp.com',
        });
        req.user = { id: payload.sub, role: payload.role };
        next();
    } catch (e) {
        return res.status(401).json({ error: e.message });
    }
}

JWT 黑名单

需要"立刻撤销 token"时,用 Redis 黑名单:

// 登出
function logout(req, res) {
    const token = req.headers.authorization.replace('Bearer ', '');
    const payload = jwt.decode(token);
    const ttl = payload.exp - Math.floor(Date.now() / 1000);
    redis.setEx(`bl:${payload.jti}`, ttl, '1');
}

// 验证中间件加一步
async function authMiddleware(req, res, next) {
    // ... 先验签名
    if (await redis.get(`bl:${payload.jti}`)) {
        return res.status(401).json({ error: 'token revoked' });
    }
    // ...
}

JWT vs Session vs PASETO

  • JWT:无状态、可跨服务,但撤销难、payload 暴露。
  • Session:有状态、立刻撤销,但需要中心存储、不能跨服务。
  • PASETO:JWT 的"安全替代品",修复 JWT 标准里几个安全缺陷(算法混淆、none 算法等),设计上"不可滥用"。

2025 年的主流仍然是 JWT —— 因为生态成熟、各语言库齐全。但要严格按最佳实践用。

常见安全问题

问题 1:Algorithm Confusion 攻击。服务用 RS256 验证,但传 "alg": "HS256" 的 token,部分库会用公钥作为 HMAC 密钥验证 —— 公钥是公开的,攻击者能伪造任意 token。修复:验证时严格指定 algorithms 数组。

问题 2:kid 注入。kid 来自 token 本身(用户可控),如果服务端拿 kid 去 SQL 查询或文件读取,可能引发 SQL 注入或路径遍历。修复:kid 当作枚举值验证,不能用作数据源标识符。

问题 3:JWT 过大。塞太多数据,header 几 KB,每个请求都带,流量浪费。规则:Payload 只放必要鉴权信息,详细数据每次查库。

JWT 在微服务里的传递

JWT 的核心价值在微服务架构里体现得最明显:

客户端 -> API Gateway -> Service A -> Service B -> Service C

# 每一跳都带着同一个 JWT(或者 Gateway 重新签发服务间 token)
# 每个服务用同一组公钥验证 JWT
# 不需要中央 Session 服务,验证完全本地

关键设计:

  • JWKS endpoint 缓存:每个服务启动时拉一次公钥,后续按 TTL 刷新(避免每次验证都查 IdP)。
  • 用 audience 区分服务:Service A 的 token 不应被 Service B 接受 —— 即使签名验证通过,aud 不对就拒。
  • 异步任务带 token:消息里带 JWT,消费方异步处理时仍能验证身份。

JWT 大小限制

HTTP header 总大小通常被限制在 4-16KB,JWT 占了大头要小心。

# 不要在 JWT 塞:
- 用户的所有 profile 字段
- 权限列表(可能几十条)
- 设备信息、IP 历史

# 应该:
- 用户 ID
- 简化的 role(admin / user / vip)
- 必要的 tenant_id 等隔离字段
# 详细信息从这些 ID 现查

JWT 与 CORS

跨域调 API 带 Authorization header,浏览器会做预检请求(OPTIONS)。服务端要正确响应:

Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Max-Age: 86400

注意:Allow-Credentials: true 时 Origin 不能用 *,必须显式指定。

JWT 调试工具

  • jwt.io:在线解析 + 验证 JWT,贴 token 上去看 header/payload/signature。
  • jwt-cli:命令行版,适合 CI / 脚本。
  • Postman:OAuth2 Helper,自动管理 token 生命周期。

注意:不要把生产环境的真 token 贴 jwt.io —— 它在浏览器里解析,理论上不上传,但敏感场景仍要小心。

写在最后

JWT 是优秀的工具但极易被误用。把它定位准确:跨服务、短期、自包含的身份凭证。该撤销时配黑名单,该长存时改 refresh_token + 短期 access_token,该多方时用 JWKS + RS256。把这些原则做到位,JWT 就是高效又安全的鉴权方案。不要为了 JWT 而 JWT —— 简单的单体应用,Session 比 JWT 简单可控,选 JWT 是给自己找麻烦。

一图看懂

JWT 认证一图看懂:

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

OAuth2 与 OIDC 完全指南:从授权码模式到 PKCE 的安全实战

2026-5-15 17:25:45

技术教程

OWASP Top 10 完全指南:Web 应用最重要的 10 类安全威胁

2026-5-15 17:25:46

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索