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

目录

关键要点

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

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

JWT 的结构

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

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

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

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

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

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 映射、iss、aud、exp、nbf、必要时的 iat 新鲜度、typ 和 kid 轮换校验。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 往往更容易审计和撤销。

延伸阅读