JSON Web Token(JWT)是 RFC 7519 定义的紧凑型声明格式。它可以通过 JWS 表示签名后的声明,也可以通过 JWE 表示加密后的声明;Base64URL 只是编码,本身不提供机密性、完整性或身份认证。签名验证了受信任签发者的声明,并不自动证明调用者有权访问任意租户或对象。

目录

关键要点

  • 结构:JWT 常以 header.payload.signature 形式出现,但 JWS/JWE 的序列化形式取决于具体协议。
  • 声明只是策略输入sub、角色和 scope 不能替代租户、对象和业务授权。
  • 验证必须显式:固定允许的算法与可信密钥,并校验 issaudexpnbftypkid 以及所需 scope。
  • 状态并没有消失:撤销、会话、密钥轮换、刷新令牌和授权判断通常都需要服务端状态。
  • 选型取决于威胁模型:API 断言、跨服务调用等场景可能适合 JWT;简单登录会话也可能更适合不透明令牌或服务端 Session。

不要把生产令牌粘贴到不受信任的解码器、日志系统或在线表单中。阅读示例时只使用合成、脱敏的 fixture,并把解码出的声明视为不可信输入。

JWT 的结构

在最常见的紧凑 JWS 表示中,三个部分由点号分隔:

部分 含义
Header 序列化类型、算法标识和可选的密钥 ID。
Payload 注册声明、公共声明或双方约定的私有声明。
Signature 对编码后的头部和载荷进行验证的结果。

示例形状为 xxxxx.yyyyy.zzzzz。头部和载荷通常使用 Base64URL 编码,因此任何取得令牌的人都可能读到它们。

json
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-2026-01"
}

algkid 来自不可信输入。验证器必须先根据受信任的签发者配置选择算法白名单和密钥集合,不能因为头部声称了某个算法就接受它。

Payload

注册声明包括 iss(签发者)、sub(主体)、aud(受众)、exp(过期时间)、nbf(生效时间)和 iat(签发时间)。公共声明应避免命名冲突;私有声明则需要调用双方明确约定。载荷不适合存放密码、支付数据或长期秘密。

json
{
  "iss": "https://issuer.example",
  "sub": "synthetic-user",
  "aud": "orders-api",
  "scope": "orders:read",
  "iat": 1760000000,
  "exp": 1760000900
}

Signature

对 JWS 而言,签名覆盖 base64url(header) + "." + base64url(payload) 的 ASCII 字节。它能检测篡改,并在密钥与签发者映射可信时认证签发者的声明;它不会授予对象访问权,也不能证明当前浏览器操作者就是 sub 所代表的人。

多语言实现

以下示例使用环境变量和批准的密钥存储,仅用于说明验证边界。生产代码还应加入密钥轮换、错误分类、审计和业务授权。

JavaScript(Node.js)

javascript
const jwt = require('jsonwebtoken');

const payload = {
  sub: 'synthetic-user',
  iss: process.env.JWT_ISSUER,
};
const token = jwt.sign(payload, process.env.JWT_SIGNING_KEY, {
  algorithm: 'RS256',
  expiresIn: '15m',
  audience: process.env.JWT_AUDIENCE,
  keyid: process.env.JWT_KEY_ID,
});

const claims = jwt.verify(token, process.env.JWT_VERIFYING_KEY, {
  algorithms: ['RS256'],
  issuer: process.env.JWT_ISSUER,
  audience: process.env.JWT_AUDIENCE,
});
// 验证通过后,仍需检查 scope、tenant、object 和业务状态。

Python(PyJWT)

python
import os
import jwt

payload = {
    "sub": "synthetic-user",
    "iss": os.environ["JWT_ISSUER"],
}
token = jwt.encode(
    payload,
    os.environ["JWT_SIGNING_KEY"],
    algorithm="RS256",
    headers={"kid": os.environ["JWT_KEY_ID"]},
)

claims = jwt.decode(
    token,
    os.environ["JWT_VERIFYING_KEY"],
    algorithms=["RS256"],
    issuer=os.environ["JWT_ISSUER"],
    audience=os.environ["JWT_AUDIENCE"],
)

Java(JJWT 0.12.x 风格 API)

java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;

PrivateKey signingKey = loadPrivateKeyFromApprovedKeyStore();
String token = Jwts.builder()
    .subject("synthetic-user")
    .issuer(System.getenv("JWT_ISSUER"))
    .audience().add(System.getenv("JWT_AUDIENCE")).and()
    .issuedAt(new Date())
    .expiration(new Date(System.currentTimeMillis() + 15 * 60 * 1000))
    .signWith(signingKey)
    .compact();

PublicKey verificationKey = loadPublicKeyFromApprovedKeyStore();
Claims claims = Jwts.parser()
    .verifyWith(verificationKey)
    .requireIssuer(System.getenv("JWT_ISSUER"))
    .requireAudience(System.getenv("JWT_AUDIENCE"))
    .build()
    .parseSignedClaims(token)
    .getPayload();
// 仍需配置算法白名单,并检查 scope、租户、对象和撤销状态。

JJWT、PyJWT 和 Node.js 库的 API 会随版本变化。应固定依赖版本,阅读对应迁移指南,并为错误算法、错误受众、过期令牌、错误租户和缺少 scope 的情况编写拒绝测试。

安全最佳实践

1. 完整校验认证上下文

服务端应先选择受信任的 issuer 配置,再解析令牌,并执行算法白名单、密钥与 issuer 映射、issaudexpnbf、必要时的 iat 新鲜度、typkid 轮换校验。sub、角色和 scope 只是授权策略的输入,不能单独决定对象或租户访问。

不要把不可信的 kid 直接拼接为文件路径、SQL 或任意远程 URL。应从受限的密钥集合或经过 issuer 固定、TLS 校验、大小限制和刷新限流的 JWKS 缓存中解析。

2. 选择存储与传输方式

所有链路使用 TLS;禁止把令牌放进 URL、查询参数、日志、分析事件或异常消息。HttpOnly Cookie 可以降低脚本直接读取令牌的风险,但不能单独防 CSRF。Cookie 认证还需要 Secure、合适的 SameSite、Origin/Referer 检查以及 CSRF Token 或等价的服务端防御。内存存储、Web Storage 和 Cookie 各有重载、XSS、CSRF、设备共享与恢复边界,没有脱离威胁模型的统一答案。

3. 把刷新与撤销设计成有状态流程

访问令牌只携带短有效期内必需的声明。刷新令牌应使用高熵随机值,在独立策略下保存和传输,每次成功使用都轮换,并维护 token family。检测到旧刷新令牌再次使用时,应撤销相关 family 并要求重新认证。登出、改密、移除租户或安全事件发生时,可通过会话版本、撤销记录或黑名单让失效立即生效。JWT 本地可验证,不代表整个会话系统天然无状态。

4. 最小化数据并做好观测

不要因为载荷方便就放入密码、支付信息、长期秘密或未经审查的动态权限。日志只记录脱敏后的令牌标识、issuer、kid 和决策原因,不记录 Bearer 凭据。测试应覆盖篡改、错误 issuer/audience、过期、时钟偏差、缺少 scope、跨租户对象访问和刷新重放。

常见问题

1. JWT 应该存在哪里?

没有通用答案。HttpOnly; Secure Cookie 需要 CSRF 防护;内存存储减少持久暴露但会影响刷新;Web Storage 可被注入脚本读取。应结合 XSS、CSRF、登出、设备共享和恢复流程决定。

2. 访问令牌过期后怎么办?

可以使用独立的刷新流程,但刷新令牌要轮换、检测重放、设置 family 过期并支持服务端撤销。有效期应根据业务风险、会话体验和部署环境校准,而不是照搬固定区间。

3. JWT、JWS 和 JWE 有什么区别?

JWT 是声明格式;JWS 是签名表示;JWE 是加密表示。常见 JWT 是 JWS,因此载荷可读但可验证;JWE 只在确有机密性需求且密钥管理成熟时使用。

4. 有效签名是否等于有权访问?

不是。有效签名只说明受信任签发者的声明未被篡改。服务端仍需根据主体、租户、对象、scope、账户状态和业务规则做授权。

5. JWT 能否撤销?

不能修改已经签发的字节。可以使用短期访问令牌、会话版本、撤销记录和刷新令牌 family;高风险场景可使用黑名单,但这会引入存储和运维状态。

总结

JWT 是紧凑的声明格式,不是授权策略,也不自动提供加密。可靠的实现需要固定算法和 issuer 密钥,完整校验声明上下文,把认证与租户/对象授权分开,并把浏览器存储、刷新、撤销和密钥轮换作为明确的系统设计。如果这些复杂度超过了场景收益,不透明令牌配合服务端 Session 往往更容易审计和撤销。

延伸阅读