JWT(JSON Web Token)是一种开放的行业标准,定义于 RFC 7519,用于在各方之间安全地传输信息。该信息以 JSON 对象的形式存在,并通过数字签名确保其完整性和可信性。签名可使用 HMAC 算法配合密钥,或基于 RSA、ECDSA 的公私钥对实现。
从工程实现角度看,JWT 本质上是一个标准化、带防伪签名且具备有效期的声明字符串,适用于分布式环境下的身份认证与信息交换。
Header.Payload.Signature
理解 JWT 需要先认识 HTTP 协议的“无状态”特性所带来的挑战。传统认证方式依赖 Session-Cookie 模型:
这种模式存在以下工程痛点:
为此,JWT 提出一种无状态、可移植的身份验证机制,将身份信息和有效期等状态从服务端转移到客户端,由客户端在每次请求中自行携带,服务端仅做校验即可完成认证。
xxxxx.yyyyy.zzzzz
一个 JWT 字符串由三部分组成,依次为 Header、Payload 和 Signature,各部分通过点号(.)连接:
<Header>.<Payload>.<Signature>
Header 是一个 JSON 对象,包含关于令牌本身的元数据。
常见字段包括:
typ (Type)
alg (Algorithm)
生成过程:将 Header JSON 对象进行 Base64Url 编码,形成 JWT 的第一段。Base64Url 是 URL 安全版本的 Base64 编码,避免出现 +、/、= 等特殊字符影响传输。
示例原始内容:
{
"alg": "HS256",
"typ": "JWT"
}
编码后结果:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload 包含一组称为“声明(Claims)”的键值对,描述了用户信息及相关元数据。
声明分为三种类型:
iss (Issuer)
sub (Subject)
aud (Audience)
exp (Expiration Time)
nbf (Not Before)
iat (Issued At)
生成过程:将 Payload JSON 对象进行 Base64Url 编码,得到 JWT 的第二段。
示例原始内容:
{
"sub": "1234567890",
"name": "John Doe",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
编码后结果:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9
Signature 的核心作用是防止数据篡改,确保 JWT 的完整性与真实性。
生成方式如下:
encodedHeader + '.' + encodedPayload;最终,接收方可以通过相同方式重新计算签名并与原 Signature 比对,验证 JWT 是否被修改。
验证令牌的真实性(Authenticity)和完整性(Integrity),确保其在传输过程中未被篡改,且确实由可信的签发方生成。
base64UrlEncode(header) + "." + base64UrlEncode(payload)。以 HS256 算法为例:
Signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret )
将计算出的签名再次进行 Base64Url 编码,即构成 JWT 的第三部分——Signature。
/login 接口提交用户名与密码。Bearer <token>
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
Authorization
无状态性与高可扩展性:
? 服务器无需维护会话状态,显著降低内存开销,天然适配分布式系统与微服务架构。
自包含性:
? 用户相关信息可嵌入载荷中,减少频繁查询数据库的需求。
解耦与跨平台支持:
? 认证逻辑独立,适用于多种客户端类型(Web、移动端、IoT设备)及跨域通信场景。
不可撤销性问题:
? JWT 一旦签发,在有效期内始终有效。若发生泄露,攻击者可在过期前持续使用。
解决方案包括:设置较短有效期、引入刷新令牌(Refresh Token)机制、或建立黑名单机制(但此方式违背了无状态设计原则)。
数据明文暴露:
? Payload 仅为 Base64Url 编码,不具备加密功能。
任何敏感信息都严禁存放在 Payload 中。
令牌体积膨胀:
? 若在 Payload 中写入过多数据,会导致 JWT 字符串过长,增加每次 HTTP 请求的传输负担。
密钥安全管理:
? 签名所用的密钥是整个安全体系的核心,
必须严格保护,绝不能暴露于客户端或日志中。
同时需避免使用不安全的算法,例如 alg: "none"。
工程红线:数据库中禁止存储明文密码。
=> 推荐使用 bcrypt 算法进行密码哈希。
采用分层结构提升代码的内聚性,降低模块间耦合度,便于单元测试与后续功能拓展。
应避免硬编码敏感信息(如密钥、算法等),需集中管理配置项。
config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
# JWT 配置项
SECRET_KEY: str = "a_very_secret_and_long_string_for_jwt" # 在生产环境中应通过环境变量设置
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # 设置访问令牌的过期时间为30分钟
class Config:
env_file = ".env" # 支持从 .env 文件中加载配置,实现灵活覆盖
settings = Settings()
该模块职责单一,专注于密码的相关操作,包括加密、校验等核心功能。
以下是 auth.py 文件中的完整实现代码:
from datetime import datetime, timedelta, timezone
from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from pydantic import BaseModel
from .config import settings
# 定义用于解析令牌数据的 Pydantic 模型
class TokenData(BaseModel):
username: Optional[str] = None
# 初始化 OAuth2 密码流模式,指定获取令牌的接口路径
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""
生成 JWT 访问令牌
:param data: 要编码进令牌的数据(即 payload 内容)
:param expires_delta: 自定义令牌有效期,若未提供则使用默认值
:return: 编码后的 JWT 字符串
"""
to_encode = data.copy()
# 确定过期时间点
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
# 添加标准声明 'exp' 表示过期时间
to_encode.update({"exp": expire})
# 使用指定密钥和算法对数据进行 JWT 编码
encoded_jwt = jwt.encode(
claims=to_encode,
key=settings.SECRET_KEY,
algorithm=settings.ALGORITHM
)
return encoded_jwt
def get_current_user(token: str = Depends(oauth2_scheme)): # -> User:
"""
解析并验证传入的 JWT 令牌,提取用户信息
此函数作为 FastAPI 的依赖项,可用于保护任意需要认证的路由
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# 尝试解码令牌
payload = jwt.decode(
token,
settings.SECRET_KEY,
algorithms=[settings.ALGORITHM]
)
# 提取主题字段 'sub',通常为用户名
username: str = payload.get("sub")
if username is None:
raise credentials_exception
# 使用模型校验结构有效性
token_data = TokenData(username=username)
# 步骤四:整合模块并构建 API 接口
将认证、密码处理与路由逻辑结合,实现完整的接口服务。
main.py
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from . import auth, password_utils
app = FastAPI()
# 公共访问接口:用户登录并获取访问令牌
@app.post("/api/auth/token")
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
# 实际项目中应通过数据库查询用户信息
# user = get_user_from_db(username=form_data.username)
# if not user:
# raise HTTPException(...)
# 演示环境下的模拟验证逻辑
# 假定仅存在一个用户名为 'admin' 的用户,密码明文为 'password'
stored_hashed_password = password_utils.get_password_hash("password")
is_valid = password_utils.verify_password(form_data.password, stored_hashed_password)
if not (form_data.username == "admin" and is_valid):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}
)
# 构造 JWT 载荷数据,'sub' 为标准注册声明,通常存储用户唯一标识
token_payload = {"sub": form_data.username}
# 调用工具函数生成 JWT 字符串
access_token = auth.create_access_token(data=token_payload)
return {"access_token": access_token, "token_type": "bearer"}
# 受权限控制的接口示例一:获取当前用户信息
@app.get("/api/users/me")
def read_users_me(current_user: auth.TokenData = Depends(auth.get_current_user)):
"""
返回已认证用户的详细信息。
FastAPI 自动完成以下流程:
1. 解析请求头中的 Authorization 字段
2. 执行 get_current_user 依赖项进行身份校验
3. 若校验失败,自动抛出 401 错误,后续代码不执行
4. 若成功,则将解析出的用户数据注入到 current_user 参数
"""
return {"username": current_user.username}
# 受权限控制的接口示例二:管理员创建文章
@app.post("/api/admin/posts")
def create_new_post(current_user: auth.TokenData = Depends(auth.get_current_user)):
# 只有成功通过 auth.get_current_user 验证的请求才能进入此逻辑
# 后续可在此添加操作 Markdown 文件等业务逻辑
return {"message": f"Hello {current_user.username}, you are authorized to create a post."}
Header.Payload.Signature
# 总结与关键点回顾
**JWT 令牌的生成方式**
使用 `create_access_token` 函数来创建安全令牌。
输入参数为一个字典,表示 JWT 中的“声明”(claims)。其中最重要的自定义声明是 `sub`,用于存放用户的唯一标识(如用户名或 ID)。
该函数在内部会自动补充标准字段,例如过期时间(exp)、签发时间(iat)等,并使用预设密钥和算法对数据进行签名,最终返回编码后的 JWT 字符串。
exp 字段用于设定令牌的有效期限,控制其生命周期。
随后,调用 jose.jwt.encode 方法,传入以下参数:
由此生成最终的 JWT 字符串。
将密码处理、身份认证与路由控制等逻辑进行分离,提升代码模块化程度,使整体结构更加清晰易维护。
借助 FastAPI 提供的 Depends 机制,构建了一个可复用的 get_current_user 依赖项。在需要用户认证的接口中,仅需在函数参数中声明该依赖,即可自动完成令牌解析与用户验证流程。
将密钥、过期时间、加密算法等敏感或易变的配置项移至外部配置文件中管理,既提高了安全性,也增强了系统在不同环境下的适应能力。
强制采用密码哈希存储机制,防止明文密码泄露;同时对令牌验证过程中的各类异常情况(如签名错误、已过期等)进行严格处理,保障系统的安全可靠性。
扫码加好友,拉您进群



收藏
