深度解析JWT认证机制

文章目录

    • [1. 什么是 JWT 与 SSO?](#1. 什么是 JWT 与 SSO?)
    • [2. JWT 的标准认证流程](#2. JWT 的标准认证流程)
    • [3. JWT 的三部分构成深度拆解](#3. JWT 的三部分构成深度拆解)
      • [3.1 头部(Header):声明令牌的"元数据"](#3.1 头部(Header):声明令牌的“元数据”)
      • [3.2 载荷(Payload):携带数据的"核心车厢"](#3.2 载荷(Payload):携带数据的“核心车厢”)
        • [① 标准声明(Registered Claims)](#① 标准声明(Registered Claims))
        • [② 公共声明(Public Claims)](#② 公共声明(Public Claims))
        • [③ 私有声明(Private Claims)](#③ 私有声明(Private Claims))
      • [3.3 签证(Signature):不可逆的"安全防伪锁"](#3.3 签证(Signature):不可逆的“安全防伪锁”)
      • 核心结构拆解对比表
    • [4. JWT 生成的 Python 完整实现](#4. JWT 生成的 Python 完整实现)
    • [5. JWT 解密与验签的 Python 完整实现](#5. JWT 解密与验签的 Python 完整实现)
      • [为什么不直接用现成的第三方库(如 `PyJWT`)?](#为什么不直接用现成的第三方库(如 PyJWT)?)
    • [6. 生产环境下的 JWT 避坑指南](#6. 生产环境下的 JWT 避坑指南)
      • [① 绝对不要在 Payload 中存放敏感信息](#① 绝对不要在 Payload 中存放敏感信息)
      • [② 密钥(Secret)的安全管理](#② 密钥(Secret)的安全管理)
      • [③ 核心痛点:无法主动使 Token 失效的问题](#③ 核心痛点:无法主动使 Token 失效的问题)

1. 什么是 JWT 与 SSO?

JWT(JSON Web Token) 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。它被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。

在传统的 Session 认证中,分布式系统需要做 Session 共享(如使用 Redis 集群同步 Session)。而 JWT 是一种无状态(Stateless)的认证机制:服务端不保存任何 Session 数据,所有的认证信息都保存在 JWT 令牌中,由客户端每次请求时携带,完美解决了分布式系统的横向扩展性问题。

2. JWT 的标准认证流程

在深入代码之前,我们先通过一张时序图清晰地了解客户端(Client)与服务端(Server)的交互逻辑:

Code snippet

  1. 登录与令牌颁发 验证账号密码 根据用户ID和角色生成 JWT 将 Token 存入 localStorage / Cookie 2. 携带令牌请求与业务鉴权 拦截请求,解析 Token 1. 验证签名是否被篡改 2. 检查是否过期 (exp) alt 验证通过 验证失败 (Token篡改/过期) 客户端 (Browser/App) 服务端 (API Server) 发送用户名 + 密码 (POST /login) 1 登录成功,返回 jwt_token 2 请求受保护的接口 (携带 Authorization: Bearer <token>) 3 返回业务数据 (200 OK) 4 返回未授权错误 (401 Unauthorized) 5 客户端 (Browser/App) 服务端 (API Server)

流程核心步骤解析:

  1. 登录请求:客户端向服务端发送用户名和密码。

  2. 生成令牌:服务端校验账号密码正确后,基于用户信息生成 JWT 令牌,并返回给客户端。服务端不需要存储这个 Token。

  3. 本地存储 :客户端将 JWT 存储在 localStoragesessionStorageCookie 中。

  4. 携带请求 :客户端后续请求受保护的接口时,在 HTTP 请求头中携带 JWT,通常放在 Authorization 字段中,格式为:Bearer <token>

  5. 验签放行 :服务端拦截请求,利用本地保存的 Secret 校验签名和有效期。验证通过则直接解析出用户信息并放行,正常返回业务数据。

3. JWT 的三部分构成深度拆解

JWT 表面上是一个由点(.)分隔的长字符串,但它的底层结构设计得非常精妙。

JWT = Base64URL(Header) + " . " + Base64URL(Payload) + " . " + Signature \text{JWT} = \text{Base64URL(Header)} + "." + \text{Base64URL(Payload)} + "." + \text{Signature} JWT=Base64URL(Header)+"."+Base64URL(Payload)+"."+Signature

3.1 头部(Header):声明令牌的"元数据"

Header 是一个 JSON 对象,用来描述这个 Token 的基本元信息。它就像是包裹上的快递单,告诉物流系统(服务器)这个包裹该怎么处理。

通常包含两个核心字段:

  • typ (Type) :令牌的类型,对于 JWT 而言,其值固定为 JWT

  • alg (Algorithm) :用来生成签名(第三部分)的加密算法。最常用的是对称加密算法 HS256(HMAC-SHA256)和非对称加密算法 RS256(RSA-SHA256)。

json 复制代码
{
  "typ": "JWT",
  "alg": "HS256"
}

底层处理 :这一段 JSON 会被使用 Base64URL 编码转换为字符串,成为 JWT 的第一部分。因为没有加密,任何人拿到第一段字符串都能通过 Base64 解码还原出这段 JSON

3.2 载荷(Payload):携带数据的"核心车厢"

Payload 也是一个 JSON 对象,是 JWT 的核心实体,用来存放真正想要传输的业务数据(即"声明" Claims)。

公共规范将 Payload 的字段分为了三类:

① 标准声明(Registered Claims)

这些是 JWT 官方推荐(但非强制)的标准字段,每个都有特定含义:

  • iss (Issuer):签发者。表示这个 Token 是谁创建的(例如:auth.yourcompany.com)。

  • sub (Subject):面向的用户。通常放用户的唯一标识(如 User ID)。

  • aud (Audience):接收方。声明这个 Token 是给哪个系统用的。

  • exp (Expiration Time):过期时间戳 (秒级)。服务器验证时发现当前时间大于 exp 就会拒绝服务。这是防范 Token 无限期生效的关键。

  • nbf (Not Before):生效时间戳。在此时间之前,该 Token 不可用。

  • iat (Issued At):签发时间戳。

  • jti (JWT ID):当前 Token 的唯一编号。通常配合 Redis 用来防止"重放攻击"(同一个 Token 被黑客反复提交)。

② 公共声明(Public Claims)

由开发者在生产环境中自行定义。为了避免命名冲突,官方建议公共声明的 Key 可以定义为类似外部可查的 URL 或带有特定命名空间的字符串(实际开发中用得较少)。

③ 私有声明(Private Claims)

业务中最常用的自定义字段。由服务端和客户端约定好,用来传递业务必不可少的非敏感信息。

json 复制代码
{
  // 标准声明
  "sub": "1234567890",
  "exp": 1782294400,
  "iat": 1782290800,
  // 私有声明(自定义业务字段)
  "name": "wangxiaoming",
  "role": "admin",
  "user_id": 1
}

铁律提醒 :Payload 与 Header 一样,同样只是通过 Base64URL 编码成了字符串(第二部分)。它依然是明文可见的! ---

3.3 签证(Signature):不可逆的"安全防伪锁"

如果说 Header 和 Payload 都是"裸奔"的明文,那 Signature 则是保证 JWT 无法被黑客篡改的核心守护神

它是如何生成的?

服务端拿着已经编码好的(Base64后的) HeaderPayload,用一根小黑点(.)拼接起来,再引入一个只有服务器知道的机密密钥(Secret) 。最后,把这三者送入 Header 中声明的 alg 算法(比如 HS256)进行哈希哈希,公式如下:

Signature = HMAC-SHA256 ( Header + " . " + Payload , secret ) \text{Signature} = \text{HMAC-SHA256}(\text{Header} + "." + \text{Payload}, \text{secret}) Signature=HMAC-SHA256(Header+"."+Payload,secret)

它是如何防伪的?
  1. 黑客改了数据 :假设黑客拦截了 Token,把 Payload 里的 role"user" 改成了 "admin"

  2. 重新编码发送:黑客把修改后的 Payload 重新用 Base64URL 编码,拼上原有的 Header 和 Signature 发给服务器。

  3. 服务器校验 :服务器收到后,取出黑客传过来的 Header 和篡改后的 Payload,加上服务器内部的 Secret在本地重新计算一次签名

  4. 对不上,穿帮! :因为黑客不知道 Secret 是什么,他伪造不出能够匹配篡改后数据的 Signature。服务器计算出的本地新签名,和黑客传过来的旧签名对不上,服务器直接抛出 401 Unauthorized 异常。

核心结构拆解对比表

组成部分 官方名称 角色定位 编码/加密方式 客户端能否解密? 允许存放敏感数据吗?
第一部分 Header 协议说明书 Base64URL 编码 (直接解开) 不允许
第二部分 Payload 业务数据集 Base64URL 编码 (直接解开) 绝对不要放密码
第三部分 Signature 防伪防篡改锁 HMAC-SHA256 签名 绝对不能(不可逆哈希) 纯哈希值,无实际业务数据

4. JWT 生成的 Python 完整实现

技术纠错提示 :标准的 JWT 签名(HS256)并不是直接将 header + payload + secret 做普通的 SHA256 哈希,而是使用 HMAC-SHA256 算法。同时,Base64 编码在 JWT 规范中使用的是 Base64URL 编码(移除 =,将 +/ 替换为 -_),避免在 URL 或 HTTP Header 传输时因特殊符号出错。

以下是完全符合官方官方规范的标准 Python 手动实现示例:

python 复制代码
import base64
import json
import hmac
import hashlib
import time

def base64url_encode(data: bytes) -> str:
    """符合 JWT 规范的 Base64URL 编码(移除末尾的 = 并替换特殊符号)"""
    return base64.urlsafe_b64encode(data).decode('utf-8').rstrip('=')

def generate_jwt():
    # 1. Header (头部)
    header_data = {"typ": "JWT", "alg": "HS256"}
    # separators=(',', ':') 用于消除空格,保证序列化后的字符串最紧凑
    header_json = json.dumps(header_data, separators=(',', ':')).encode('utf-8')
    header = base64url_encode(header_json)

    # 2. Payload (载荷)
    current_time = int(time.time())
    payload_data = {
        "sub": "root",
        "exp": current_time + 3600,  # 1小时后过期
        "iat": current_time,          # 签发时间
        "name": "wangxiaoming",
        "user_id": 1,
        "admin": True
    }
    payload_json = json.dumps(payload_data, separators=(',', ':')).encode('utf-8')
    payload = base64url_encode(payload_json)

    # 3. Signature (签证)
    # 服务端密钥,绝不可泄露
    secret = b'django-insecure-hbcv-y9ux0&8qhtkgmh1skvw#v7ru%t(z-#chw#9g5x1r3z=$p'
    
    # 签名原始数据:header.payload
    signing_input = f"{header}.{payload}".encode('utf-8')
    
    # 使用 HMAC-SHA256 算法生成标准签名
    signature_bytes = hmac.new(secret, signing_input, digestmod=hashlib.sha256).digest()
    signature = base64url_encode(signature_bytes)

    # 4. 拼接完整 JWT Token
    jwt_token = f"{header}.{payload}.{signature}"
    return jwt_token

if __name__ == '__main__':
    token = generate_jwt()
    print("生成的标准 JWT Token:\n", token)

5. JWT 解密与验签的 Python 完整实现

在实际业务中,当客户端带着 Token 过来请求资源时,服务端需要做三件事:

  1. 切割字符串 :拿到 Header, Payload, Signature

  2. 验签(防篡改) :用同样的 Secret 和算法对 Header.Payload 重新计算签名,对比客户端传过来的是否一致。

  3. 验过期(防超时) :解出 Payload,对比当前时间戳是否大于 exp

技术提示 :解密(Verify)过程中,最精妙的地方在于必须使用"恒定时间字符串比较" (如 Python 的 hmac.compare_digest)。这是为了防止黑客通过计时攻击(Timing Attack),根据服务器返回错误的速度一点点猜出正确的签名。

python 复制代码
import base64
import json
import hmac
import hashlib
import time

# 依然使用我们生成 Token 时的那个唯一密钥
SECRET = b'django-insecure-hbcv-y9ux0&8qhtkgmh1skvw#v7ru%t(z-#chw#9g5x1r3z=$p'

def base64url_decode(payload: str) -> bytes:
    """符合 JWT 规范的 Base64URL 解码"""
    # 补齐 Base64 编码所需的 '='
    rem = len(payload) % 4
    if rem > 0:
        payload += '=' * (4 - rem)
    return base64.urlsafe_b64decode(payload.encode('utf-8'))

def verify_jwt(token: str) -> dict:
    """
    校验并解密 JWT 令牌
    :param token: 客户端传来的完整 JWT 字符串
    :return: 解析成功返回 Payload 字典,失败则抛出异常或返回 None
    """
    try:
        # 1. 按点(.)拆分三部分
        parts = token.split('.')
        if len(parts) != 3:
            raise ValueError("非法的 JWT 格式")
            
        header_segment, payload_segment, crypto_segment = parts
        
        # 2. 重新计算签名(验签逻辑)
        # 签名的原始数据是前两段拼接
        signing_input = f"{header_segment}.{payload_segment}".encode('utf-8')
        
        # 服务端本地计算新签名
        expected_signature_bytes = hmac.new(SECRET, signing_input, digestmod=hashlib.sha256).digest()
        
        # 标准的 Base64URL 编码签名
        # 移除末尾的 '='
        expected_signature = base64.urlsafe_b64encode(expected_signature_bytes).decode('utf-8').rstrip('=')
        
        # 3. 安全对比签名(防计时攻击)
        if not hmac.compare_digest(crypto_segment, expected_signature):
            raise ValueError("签名验证失败,Token 已被篡改!")
            
        # 4. 签名无误后,解密 Payload 拿到业务数据
        payload_json = base64url_decode(payload_segment)
        payload = json.loads(payload_json.decode('utf-8'))
        
        # 5. 校验过期时间(exp)
        current_time = int(time.time())
        if 'exp' in payload and current_time > payload['exp']:
            raise ValueError("Token 已过期,请重新登录")
            
        print("鉴权成功!")
        return payload

    except Exception as e:
        print(f"鉴权失败: {e}")
        return None

if __name__ == '__main__':
    # 模拟测试:假设这是从生成函数拿到的合法的 Token
    # 这里直接调用上一章生成的 Token 进行测试
    from __main__ import generate_jwt  # 如果你在同一个文件里
    valid_token = generate_jwt()
    
    print("--- 场景1:正常携带合法 Token 请求 ---")
    user_info = verify_jwt(valid_token)
    if user_info:
        print("解析出的用户信息:", user_info)
        
    print("\n--- 场景2:黑客拦截并篡改 Payload 后请求 ---")
    # 模拟黑客把明文部分的 payload 替换掉(比如伪造 user_id)
    token_parts = valid_token.split('.')
    fake_payload = {"sub": "root", "user_id": 999, "admin": True} # 试图把自己改成管理员
    fake_payload_encoded = base64.urlsafe_b64encode(json.dumps(fake_payload).encode()).decode().rstrip('=')
    
    # 拼接黑客篡改后的 Token 发送给服务器
    hacked_token = f"{token_parts}.{fake_payload_encoded}.{token_parts}"
    
    verify_jwt(hacked_token)

为什么不直接用现成的第三方库(如 PyJWT)?

发博客时,你可以特别加上这一段总结:

"虽然在实际的 Python(Django/Flask/FastAPI)开发中,我们通常直接引入 PyJWT 库(jwt.encode()jwt.decode())。但自己手动用标准库造一遍轮子,才能真正把 Base64URL、HMAC-SHA256 算法和防篡改的闭环逻辑彻底吃透。搞懂了这个底层逻辑,换到任何一门语言(Java/Go/Node.js),你的鉴权思路都是通透的!"

6. 生产环境下的 JWT 避坑指南

在实际业务落地时,JWT 有几个非常核心的安全隐患和痛点,这也是技术面试中最常问的高频题:

① 绝对不要在 Payload 中存放敏感信息

正如前文解码分析所示,Header 和 Payload 仅仅只是做了 Base64 编码,并没有加密!任何人拿到 Token 都可以直接通过网页工具解码看到里面的明文数据。

  • 错误做法:在 Payload 中存放密码、身份证号、银行卡号。

  • 正确做法:只存放不敏感的用户唯一标识(如 UUID、UserID)或业务权限标签。

② 密钥(Secret)的安全管理

签名的大前提是 Secret 只有服务器知道。如果私钥泄露,黑客就可以在外面任意篡改 Payload 并生成合法的签名,直接接管整个系统的最高权限。

  • 安全建议 :在生产环境中,务必将 Secret 写入服务器的环境变量中(如 .env),严禁直接硬编码在代码里提交到 GitHub

③ 核心痛点:无法主动使 Token 失效的问题

由于 JWT 是无状态的,一经签发,在到期(exp)之前它都是绝对合法的。如果中途用户修改了密码,或者管理员想强制恶意用户下线,服务端由于不保存状态,无法直接宣布某个 Token 废弃。

  • 主流企业级解决方案

    • 黑名单机制:将注销或失效的 Token 放入 Redis 缓存中,设置过期时间为 Token 的剩余寿命。每次鉴权时先查一下 Redis,如果在黑名单中则拒绝。该方案虽然引入了状态,但只在注销时写缓存,依然减轻了数据库压力。

    • 双 Token 机制 :服务端同时颁发两个 Token。一个短寿命的 AccessToken(如15分钟),负责高频的业务请求鉴权;一个长寿命的 RefreshToken(如7天),专门负责在 AccessToken 过期后异步换取新的 AccessToken。这样既保证了安全,又维持了无状态的优势。

相关推荐
llxxyy卢16 分钟前
polar夏季赛部分题目
开发语言·python
闵孚龙17 分钟前
PyTorch 系列 之 nn.Module:所有模型的骨架
人工智能·pytorch·python
AI玫瑰助手18 分钟前
Python模块:from...import...导入指定内容
开发语言·python·信息可视化
小森林之主26 分钟前
Python re 模块速查:从实战对比中掌握正则表达式
python·正则表达式·性能测试·re模块·编程实战
郭wes代码44 分钟前
Win10 拒绝访问、长期关机自动维护与声音图标灰色故障解决记录
windows·python·开源
伊布拉西莫1 小时前
LangChain LCEL源码深度剖析
python·langchain
用心_承载未来1 小时前
从“复制链接→打开APP“到“一键解析“:我做了个短视频去水印工具
python·去水印·短视频去水印
TYUT_xiaoming1 小时前
yolo模型训练
人工智能·python·yolo
MageGojo2 小时前
百度热搜API接入实战:数据结构解析与工程化调用指南
python·数据抓取·api集成·热点数据·接口调试
TechWayfarer2 小时前
查IP归属地接入实战:保险理赔如何做动态风险监控与预警
网络·python·tcp/ip·安全·flask