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 编码,因此任何取得令牌的人都可能读到它们。
Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2026-01"
}
alg 和 kid 来自不可信输入。验证器必须先根据受信任的签发者配置选择算法白名单和密钥集合,不能因为头部声称了某个算法就接受它。
Payload
注册声明包括 iss(签发者)、sub(主体)、aud(受众)、exp(过期时间)、nbf(生效时间)和 iat(签发时间)。公共声明应避免命名冲突;私有声明则需要调用双方明确约定。载荷不适合存放密码、支付数据或长期秘密。
{
"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)
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)
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)
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 往往更容易审计和撤销。