Quick Reference
| Topic | File |
|---|
| Algorithm selection | INLINECODE0 |
| Token lifecycle |
lifecycle.md |
| Validation checklist |
validation.md |
| Common attacks |
attacks.md |
Security Fundamentals
- - JWTs are signed, not encrypted—anyone can decode and read the payload; never store secrets in it
- Always verify signature before trusting claims—decode without verify is useless for auth
- The
alg: none attack: reject tokens with algorithm "none"—some libraries accepted unsigned tokens - Use strong secrets: HS256 needs 256+ bit key; short secrets are brute-forceable
Algorithm Choice
- - HS256 (HMAC): symmetric, same key signs and verifies—good for single service
- RS256 (RSA): asymmetric, private key signs, public verifies—good for distributed systems
- ES256 (ECDSA): smaller signatures than RSA, same security—preferred for size-sensitive cases
- Never let the token dictate algorithm—verify against expected algorithm server-side
Required Claims
- -
exp (expiration): always set and verify—tokens without expiry live forever - INLINECODE6 (issued at): when token was created—useful for invalidation policies
- INLINECODE7 (not before): token not valid until this time—for scheduled access
- Clock skew: allow 30-60 seconds leeway when verifying time claims
Audience & Issuer
- -
iss (issuer): who created the token—verify to prevent cross-service token theft - INLINECODE9 (audience): intended recipient—API should reject tokens for other audiences
- INLINECODE10 (subject): who the token represents—typically user ID
- Token confusion attack: without aud/iss validation, token for Service A works on Service B
Token Lifecycle
- - Access tokens: short-lived (5-15 min)—limits damage if stolen
- Refresh tokens: longer-lived, stored securely—used only to get new access tokens
- Refresh token rotation: issue new refresh token on each use, invalidate old one
- Revocation is hard—JWTs are stateless; use short expiry + refresh, or maintain blacklist
Storage
- - httpOnly cookie: immune to XSS, but needs CSRF protection
- localStorage: vulnerable to XSS, but simpler for SPAs
- Memory only: most secure, but lost on page refresh
- Never store in URL parameters—visible in logs, history, referrer headers
Validation Checklist
- - Verify signature with correct algorithm (don't trust header's alg)
- Check
exp is in future (with clock skew tolerance) - Check
iat is not unreasonably old (optional policy) - Verify
iss matches expected issuer - Verify
aud includes your service - Check
nbf if present
Common Mistakes
- - Storing sensitive data in payload—it's just base64, not encrypted
- Huge payloads—JWTs go in headers; many servers limit header size to 8KB
- No expiration—indefinite tokens are security nightmares
- Same secret across environments—dev tokens work in production
- Logging tokens—they're credentials; treat as passwords
Key Rotation
- - Use
kid (key ID) claim to identify which key signed the token - JWKS (JSON Web Key Set) endpoint for public key distribution
- Overlap period: accept old key while transitioning to new
- After rotation, old tokens still valid until they expire—plan accordingly
Implementation
- - Use established libraries—don't implement JWT parsing yourself
- Libraries:
jsonwebtoken (Node), PyJWT (Python), java-jwt (Java), golang-jwt (Go) - Middleware should reject invalid tokens early—before any business logic
快速参考
| 主题 | 文件 |
|---|
| 算法选择 | algorithms.md |
| 令牌生命周期 |
lifecycle.md |
| 验证清单 | validation.md |
| 常见攻击 | attacks.md |
安全基础
- - JWT 是签名而非加密——任何人都能解码并读取载荷;切勿在其中存储机密信息
- 在信任声明前务必验证签名——未经验证的解码对认证毫无意义
- alg: none 攻击:拒绝算法为none的令牌——某些库曾接受未签名令牌
- 使用强密钥:HS256 需要 256 位以上密钥;短密钥易被暴力破解
算法选择
- - HS256(HMAC):对称算法,同一密钥用于签名和验证——适用于单一服务
- RS256(RSA):非对称算法,私钥签名,公钥验证——适用于分布式系统
- ES256(ECDSA):签名比 RSA 更短,安全性相同——适用于对大小敏感的场景
- 绝不让令牌决定算法——在服务端根据预期算法进行验证
必需声明
- - exp(过期时间):始终设置并验证——无过期时间的令牌永久有效
- iat(签发时间):令牌创建时间——用于失效策略
- nbf(生效时间):令牌在此时间之前无效——用于定时访问
- 时钟偏差:验证时间声明时允许 30-60 秒的容差
受众与签发者
- - iss(签发者):令牌创建者——验证以防止跨服务令牌盗用
- aud(受众):预期接收方——API 应拒绝其他受众的令牌
- sub(主题):令牌代表的对象——通常为用户 ID
- 令牌混淆攻击:若无 aud/iss 验证,服务 A 的令牌可在服务 B 上使用
令牌生命周期
- - 访问令牌:短生命周期(5-15 分钟)——被盗时限制损害范围
- 刷新令牌:生命周期较长,安全存储——仅用于获取新访问令牌
- 刷新令牌轮换:每次使用时签发新刷新令牌,使旧令牌失效
- 撤销困难——JWT 无状态;使用短过期时间+刷新机制,或维护黑名单
存储
- - httpOnly Cookie:免疫 XSS,但需要 CSRF 防护
- localStorage:易受 XSS 攻击,但对 SPA 更简单
- 仅内存:最安全,但页面刷新后丢失
- 切勿存储在 URL 参数中——日志、历史记录、引用头中可见
验证清单
- - 使用正确算法验证签名(不信任头部的 alg)
- 检查 exp 在未来(考虑时钟偏差容差)
- 检查 iat 未异常久远(可选策略)
- 验证 iss 与预期签发者匹配
- 验证 aud 包含您的服务
- 检查 nbf(如存在)
常见错误
- - 在载荷中存储敏感数据——它只是 base64 编码,未加密
- 载荷过大——JWT 放在头部;许多服务器限制头部大小为 8KB
- 无过期时间——永久令牌是安全噩梦
- 跨环境使用相同密钥——开发令牌可在生产环境使用
- 记录令牌——它们是凭证;应像密码一样对待
密钥轮换
- - 使用 kid(密钥 ID)声明标识签名令牌的密钥
- JWKS(JSON Web 密钥集)端点用于分发公钥
- 重叠期:过渡到新密钥时接受旧密钥
- 轮换后,旧令牌在过期前仍有效——据此规划
实现
- - 使用成熟库——不要自行实现 JWT 解析
- 库:jsonwebtoken(Node)、PyJWT(Python)、java-jwt(Java)、golang-jwt(Go)
- 中间件应在执行业务逻辑前尽早拒绝无效令牌