一篇看懂JWT:Web安全的“身份证”

36 阅读7分钟

诸神缄默不语-个人技术博文与视频目录

什么是JWT?一个简单的比喻

想象一下你去参加一个大型会议。第一次入场时,工作人员检查你的购票信息,确认无误后给你戴上一个手环。之后在会议期间,你进出各个分会场、领取茶歇、参加活动,只需要亮出手环就可以了,不需要反复出示购票凭证。

JWT(JSON Web Token)就是这个数字世界的“手环”。它是一种让Web应用安全传递信息的方式,解决了“如何证明你是你”的问题。

为什么需要JWT?

在传统网站中,服务器通过“会话”(Session)记录用户登录状态。但这有几个问题:

  1. 服务器需要存储大量会话数据,用户多了内存压力大
  2. 难以扩展,多台服务器之间要同步会话信息
  3. 不适合移动端和API服务

JWT的出现解决了这些问题:信息都存在令牌里,服务器不用存,只需要验证令牌是否有效即可。

JWT长什么样?

一个JWT看起来像这样(实际是一长串,这里折行显示):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

它由三部分组成,用点(.)分隔:

  • 头部(Header)

  • 载荷(Payload)

  • 签名(Signature)

1. 头部(Header)

就像信封的“说明标签”,告诉别人这个令牌的基本信息:

{
  "alg": "HS256",  // 签名算法:HS256
  "typ": "JWT"     // 类型:JWT
}

2. 载荷(Payload)

这是令牌的“核心内容”,存放实际要传递的信息:

{
  "sub": "1234567890",      // 用户ID
  "name": "John Doe",       // 用户名
  "iat": 1516239022,        // 签发时间
  "exp": 1516242622         // 过期时间
}

3. 签名(Signature)

这是最关键的部分!它像“防伪标识”,确保令牌没有被篡改。

签名的生成方式:

HMACSHA256(
  base64UrlEncode(头部) + "." + base64UrlEncode(载荷),
  密钥
)

用Python玩转JWT

让我们通过代码实际体验一下JWT的使用。首先安装必要的库:

pip install PyJWT

场景1:用户登录后生成JWT

import jwt
import datetime

# 密钥(重要!实际项目中要从安全的地方获取)
SECRET_KEY = "my_secret_key_12345"

def create_jwt(user_id: str, username: str) -> str:
    """
    创建JWT令牌
    """
    # 设置令牌的过期时间(例如:24小时后)
    expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=24)
    
    # 构建载荷(Payload)
    payload = {
        "user_id": user_id,
        "username": username,
        "exp": expiration,  # 过期时间
        "iat": datetime.datetime.now(datetime.timezone.utc)  # 签发时间
    }
    
    # 生成JWT
    token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
    return token

# 示例:用户登录成功后生成令牌
token = create_jwt("user123", "张三")
print("生成的JWT令牌:")
print(token)
print("-" * 50)

场景2:验证收到的JWT

import jwt
from typing import Dict

# 密钥(重要!实际项目中要从安全的地方获取)
SECRET_KEY = "my_secret_key_12345"

def verify_jwt(token: str) -> Dict:
    """
    验证JWT令牌
    返回解码后的数据或抛出异常
    """
    try:
        # 验证并解码令牌
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        return {
            "valid": True,
            "data": payload,
            "message": "令牌有效"
        }
    except jwt.ExpiredSignatureError:
        return {
            "valid": False,
            "data": None,
            "message": "令牌已过期"
        }
    except jwt.InvalidTokenError:
        return {
            "valid": False,
            "data": None,
            "message": "无效的令牌"
        }

# 示例:验证令牌
print("验证令牌结果:")
result = verify_jwt(token)  # 在这里输入上一节返回的JWT token

if result["valid"]:
    print("✓ 令牌有效")
    print(f"用户信息:{result['data']}")
else:
    print(f"✗ {result['message']}")
print("-" * 50)

输出:

验证令牌结果:
✓ 令牌有效
用户信息:{'user_id': 'user123', 'username': '张三', 'exp': 1766732992, 'iat': 1766646592}

场景3:完整的登录验证流程

import jwt
import datetime
from typing import Dict

# 密钥(重要!实际项目中要从安全的地方获取)
SECRET_KEY = "my_secret_key_12345"

def create_jwt(user_id: str, username: str) -> str:
    """
    创建JWT令牌
    """
    # 设置令牌的过期时间(例如:24小时后)
    expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=24)
    
    # 构建载荷(Payload)
    payload = {
        "user_id": user_id,
        "username": username,
        "exp": expiration,  # 过期时间
        "iat": datetime.datetime.now(datetime.timezone.utc)  # 签发时间
    }
    
    # 生成JWT
    token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
    return token

def verify_jwt(token: str) -> Dict:
    """
    验证JWT令牌
    返回解码后的数据或抛出异常
    """
    try:
        # 验证并解码令牌
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        return {
            "valid": True,
            "data": payload,
            "message": "令牌有效"
        }
    except jwt.ExpiredSignatureError:
        return {
            "valid": False,
            "data": None,
            "message": "令牌已过期"
        }
    except jwt.InvalidTokenError:
        return {
            "valid": False,
            "data": None,
            "message": "无效的令牌"
        }

# 模拟用户数据库
users_db = {
    "user123": {
        "password": "password123",  # 实际中应该存储哈希值,而不是明文!
        "username": "张三",
        "role": "user"
    },
    "admin001": {
        "password": "admin_pass",
        "username": "管理员",
        "role": "admin"
    }
}

def login_and_get_token(user_id: str, password: str):
    """
    模拟登录过程
    """
    # 1. 检查用户是否存在
    if user_id not in users_db:
        return None, "用户不存在"
    
    # 2. 验证密码
    if users_db[user_id]["password"] != password:
        return None, "密码错误"
    
    # 3. 生成JWT令牌
    user_info = users_db[user_id]
    token = create_jwt(user_id, user_info["username"])
    
    return token, "登录成功"

def access_protected_resource(token: str):
    """
    访问需要权限的资源
    """
    result = verify_jwt(token)
    
    if not result["valid"]:
        return f"访问被拒绝:{result['message']}"
    
    user_data = result["data"]
    return f"欢迎 {user_data['username']}!您已成功访问受保护资源。"

# 模拟完整流程
print("=== 完整登录访问流程 ===")

# 1. 用户登录
print("1. 用户登录...")
token, message = login_and_get_token("user123", "password123")
print(f"登录结果:{message}")
if token:
    print(f"获取到的令牌:{token[:50]}...")
print("-" * 30)

# 2. 访问受保护资源
print("2. 访问受保护资源...")
if token:
    response = access_protected_resource(token)
    print(response)
print("-" * 30)

# 3. 演示过期令牌
print("3. 演示过期令牌...")
# 创建一个立即过期的令牌
expired_payload = {
    "user_id": "user123",
    "username": "张三",
    "exp": datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=1),  # 1秒前过期
    "iat": datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=1)
}
expired_token = jwt.encode(expired_payload, SECRET_KEY, algorithm="HS256")
response = access_protected_resource(expired_token)
print(response)

输出:

=== 完整登录访问流程 ===
1. 用户登录...
登录结果:登录成功
获取到的令牌:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkI...
------------------------------
2. 访问受保护资源...
欢迎 张三!您已成功访问受保护资源。
------------------------------
3. 演示过期令牌...
访问被拒绝:令牌已过期

JWT的实际应用场景

1. 单点登录(SSO)

用户在一个系统登录后,无需在其他关联系统重新登录。

2. API身份验证

移动App、前端应用调用后端API时携带JWT。

3. 信息交换

安全地在各方之间传递信息,因为签名可以验证内容是否被篡改。

重要安全注意事项

应该做的:

  1. 使用HTTPS:防止令牌在传输中被窃取

  2. 设置合理的过期时间:通常几小时到几天

  3. 存储敏感信息要加密:载荷默认只是编码,不是加密!

  4. 密钥要足够复杂:并且定期更换

不要做的:

  1. 不要在JWT中存储密码等敏感信息

  2. 不要将密钥硬编码在代码中

  3. 不要使用弱签名算法

  4. 前端存储要注意XSS攻击(考虑使用HttpOnly Cookie)

常见问题解答

Q:JWT和Session有什么区别?

A:Session把用户状态存在服务器,JWT把状态存在令牌里发给客户端。

Q:JWT被偷了怎么办?

A:就像手环被偷一样,小偷可以冒充你。因此过期时间要短,重要操作需二次验证。

Q:如何让JWT失效?

A:JWT一旦签发,在过期前无法主动失效。解决方案:使用短有效期+刷新令牌机制,或维护一个小的令牌黑名单。

总结

JWT就像数字世界的“身份证+防伪标识”:

  • 头部说明类型和算法

  • 载荷携带实际信息

  • 签名确保不被篡改

它的优点是无状态、易扩展,适合现代分布式应用。但也要注意安全使用,特别是密钥管理和令牌存储。

希望这篇介绍能帮你理解JWT!在实际项目中,合理使用JWT能让你的应用更安全、更高效。


提示:本文示例代码用于学习演示,实际生产环境中需要考虑更多安全因素。建议使用成熟的认证库(如Authlib、Django REST Framework JWT等)来处理复杂的认证场景。