JWT (JSON Web Token) 综合指南
1. 什么是 JWT?
JWT (JSON Web Token) 是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息。这些信息采用 JSON 对象的形式,由于经过数字签名,因此可以被验证和信任。
一个 JWT 由三部分组成,并由点(.
)分隔:
- 头部 (Header):通常包含令牌的类型(
typ
,即 “JWT”)和所使用的签名算法(alg
,如HS256
或RS256
)。 - 载荷 (Payload):包含“声明 (Claims)”,这些是关于实体(通常是用户)和其他元数据的陈述。
- 签名 (Signature):用于验证消息在传输过程中没有被篡改,并且对于使用私钥签名的令牌,它还可以验证 JWT 的发送者是谁。
一个 JWT 的示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
2. JWT 的核心安全机制:签名而非加密
一个常见的误解是认为 JWT 是加密的。实际上,标准的 JWT(JWS)的头部和载荷部分仅仅是经过 Base64Url 编码,任何人都可以轻松解码并读取其中的内容。
JWT 的安全性核心在于其签名部分。
签名的作用是:
- 数据完整性验证:接收方可以使用密钥重新计算签名,并与 JWT 中附加的签名进行比对。如果一致,则证明载荷内容自签发以来未被篡改。
- 身份认证:如果使用非对称算法(如 RS256),接收方可以用公钥验证签名,从而确认令牌是由持有相应私钥的签发者创建的。
打个比方:JWT 就像一个透明但用火漆密封的信封。任何人都能看到信里的内容(Base64Url 解码),但完好无损的火漆印章(签名)证明了信件确实来自发件人,并且内容没有被中间人修改过。
3. 为什么系统广泛使用 JWT?
尽管载荷可读,JWT 依然备受欢迎,主要得益于以下优势:
- 无状态认证 (Stateless Authentication):服务器端无需存储 Session 信息。所有验证所需的数据都包含在令牌本身,这使得系统非常容易水平扩展,完美契合微服务和分布式架构。
- 自包含与高效 (Self-Contained & Efficient):令牌包含了验证所需的所有信息(如用户ID、角色),减少了对数据库的频繁查询,提升了性能。
- 紧凑与轻量 (Compact & Lightweight):相比于 XML 格式的 SAML 令牌,JSON 格式更简洁,编码后体积更小,便于在 HTTP 请求头或 URL 中传输。
- 跨域支持 (Cross-Domain Support):可以轻松地在不同的服务或域之间传递和使用,是实现单点登录(SSO)的理想选择。
- 技术无关性:JWT 是一个开放标准,几乎所有主流编程语言都有成熟的库支持。
4. JWT 存储信息最佳实践
由于载荷是公开可读的,选择在其中存储什么信息至关重要。
可以存储哪些信息
适合存储非敏感的、用于身份识别和授权的数据。
- 注册声明 (Registered Claims):
sub
(Subject): 令牌的主题,通常是用户的唯一标识符(推荐使用 UUID 而非自增 ID)。exp
(Expiration Time): 令牌的过期时间戳,必须设置以保证安全。iss
(Issuer): 令牌的签发者。iat
(Issued At): 令牌的签发时间。
- 自定义声明 (Custom Claims):
- 用户角色和权限: 如
"role": "admin"
或"permissions": ["read", "write"]
。 - 非敏感的用户信息: 如用户名、昵称等。
- 用户角色和权限: 如
不应存储哪些信息
绝对不能在 JWT 载荷中存储任何敏感或机密信息。
- 密码或凭证: 包括用户密码(即使是哈希过的)、API 密钥等。
- 敏感的个人身份信息 (PII): 如身份证号、银行卡号、家庭住址、电话号码等。
- 机密的业务数据: 任何不应公开的内部数据。
- 大量数据: JWT 设计初衷是紧凑,过大的载荷会增加网络开销,甚至超出 HTTP 头的长度限制。
注意:如果必须在令牌中传输敏感信息,应使用 JWE (JSON Web Encryption),它会对载荷进行加密,而不仅仅是编码。
5. Python 3 实现 JWT 案例
以下是使用 PyJWT
库生成和解析 JWT 的一个完整示例。
步骤一:安装库
pip install PyJWT
步骤二:示例代码
import jwt
import datetime
import time# 1. 定义密钥和算法
# 在生产环境中,密钥必须严格保密,并从安全的位置(如环境变量)加载
SECRET_KEY = 'your-very-secret-and-secure-key'
ALGORITHM = 'HS256'# 2. 准备载荷 (Payload)
payload_data = {'sub': 'user-12345','role': 'editor','exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1),'iat': datetime.datetime.now(datetime.timezone.utc)
}print(f"[*] 原始载荷: {payload_data}\n")try:# 3. 编码生成 JWTencoded_jwt = jwt.encode(payload=payload_data,key=SECRET_KEY,algorithm=ALGORITHM)print(f"[*] 生成的 JWT: {encoded_jwt}\n")# 4. 解码并验证 JWT# jwt.decode 会自动验证签名和过期时间 (exp)decoded_payload = jwt.decode(jwt=encoded_jwt,key=SECRET_KEY,algorithms=[ALGORITHM])print(f"[*] 解码后的载荷: {decoded_payload}\n")# 5. 处理可能的异常
except jwt.ExpiredSignatureError:print("[!] 错误: 令牌已过期!")
except jwt.InvalidTokenError as e:print(f"[!] 错误: 无效的令牌!原因: {e}")
代码解析
- 密钥 (
SECRET_KEY
): 这是 JWT 安全的基石。泄露密钥意味着任何人都可以伪造有效的令牌。 jwt.encode()
: 将载荷、密钥和算法结合起来,生成一个签了名的 JWT 字符串。jwt.decode()
: 这是一个验证和解码的过程。它会首先检查签名是否有效(使用相同的密钥),然后检查exp
等声明是否满足要求。只有所有验证都通过,它才会返回解码后的载荷。
6. 关键安全提示
- 密钥安全:始终将密钥视为最高机密。不要硬编码在代码中。
- 使用强算法:推荐使用
HS256
或更强的RS256
(非对称加密)。避免使用none
算法。 - 设置过期时间 (
exp
):为所有令牌设置一个合理的、较短的生命周期,以降低令牌泄露后的风险。 - 始终通过 HTTPS 传输:防止令牌在传输过程中被中间人截获。
- 不要存储敏感数据:牢记载荷是可读的。
7. 结论
JWT 是一种强大而灵活的工具,尤其适用于现代的分布式和无状态应用架构。它的安全性不依赖于隐藏信息,而是依赖于可验证的数字签名来确保信息的完整性和真实性。正确理解并遵循其安全实践,可以构建出既高效又安全的认证和授权系统。