FastAPI + JWT 全链路实现


依赖安装

 
pip install "python-jose[cryptography]" passlib[bcrypt] python-multipart
 
用途
python-joseJWT 签发 (encode) + 验证 (decode)
passlib密码哈希(bcrypt 算法)
python-multipartFastAPI 解析 form-data(登录接口用)

第一步:密码哈希 — 永远不存明文

 
from passlib.context import CryptContext
 
# 创建密码上下文 — bcrypt 是业界标准
 
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 
# 哈希密码 — 注册时用
 
plain_password = "my_secret_123"
 
hashed = pwd_context.hash(plain_password)
 
# 验证密码 — 登录时用
 
is_valid = pwd_context.verify("my_secret_123", hashed)   # → True
 
is_valid = pwd_context.verify("wrong_password", hashed)  # → False
 

flowchart LR

    subgraph BCRYPT["bcrypt 哈希结构"]

        A["$2b$<br/>算法标识"] --> B["12<br/>轮数 (2¹² 次迭代)<br/>越大越慢越安全"]

        B --> C["KIXxNcLxH6qQ5YqJqz6ZqeT8zYOzLf5G<br/>盐 (前 22 字符,每次随机生成)<br/>+ 哈希值 (后 31 字符)"]

    end

    style A fill:#1a1a2e,stroke:#e94560,color:#eee

    style B fill:#16213e,stroke:#e94560,color:#eee

    style C fill:#0f3460,stroke:#e94560,color:#eee

 
为什么用 bcrypt 而不用 SHA256?
 
SHA256("password123") → 永远是同一个结果
 
  → 可以建彩虹表(大量常见密码的 SHA256 结果)
 
  → 数据库泄露后,查表秒破
 
bcrypt("password123") → 每次结果不同(随机盐)
 
  → 无法建彩虹表
 
  → 验证时 bcrypt 内部把盐取出来,用同样的盐哈希输入的密码再比较
 
  → 2^12 轮迭代,故意慢(~0.3s),暴力破解成本极高
 

第二步:JWT 签发 + 验证

 
from datetime import datetime, timedelta, timezone
 
from jose import JWTError, jwt
 
from typing import Optional
 
# ============================================================
 
SECRET_KEY = "a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6"
 
ALGORITHM = "HS256"
 
ACCESS_TOKEN_EXPIRE_MINUTES = 30
 
REFRESH_TOKEN_EXPIRE_DAYS = 7
 
# ============================================================
 
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
 
    to_encode = data.copy()
 
    if expires_delta:
 
        expire = datetime.now(timezone.utc) + expires_delta
 
    else:
 
        expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
 
    to_encode.update({"exp": expire, "iat": datetime.now(timezone.utc)})
 
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
 
    return encoded_jwt
 
# ============================================================
 
def verify_access_token(token: str) -> dict:
 
    try:
 
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
 
        return payload
 
    except JWTError:
 
        raise
 
# ============================================================
 
def create_refresh_token(data: dict) -> str:
 
    to_encode = data.copy()
 
    expire = datetime.now(timezone.utc) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
 
    to_encode.update({
 
        "exp": expire,
 
        "iat": datetime.now(timezone.utc),
 
        "type": "refresh",
 
    })
 
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
 

flowchart TB

    subgraph ENC["签发 (encode)"]

        direction LR

        E1["payload + secret_key"] --> E2["signature"]

        E2 --> E3["拼成 header.payload.signature<br/>→ 返回 token 字符串"]

    end

    subgraph DEC["验证 (decode)"]

        DEC_T["token 字符串"] --> DEC_A["解码 header → 拿到 alg"]

        DEC_T --> DEC_B["解码 payload → 检查 exp 是否过期"]

        DEC_T --> DEC_C["用同样的 secret_key 重新算签名 → 比对"]

        DEC_C -->|匹配| DEC_OK["✅ 返回 payload"]

        DEC_C -->|不匹配| DEC_FAIL["❌ 抛 JWTError(被篡改过!)"]

    end


第三步:FastAPI 依赖注入

 
from fastapi import Depends, HTTPException, status
 
from fastapi.security import OAuth2PasswordBearer
 
# 定义 Token 提取器 — 从请求头 Authorization: Bearer <token> 拔 token
 
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
 
async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
 
    credentials_exception = HTTPException(
 
        status_code=status.HTTP_401_UNAUTHORIZED,
 
        detail="无法验证凭据",
 
        headers={"WWW-Authenticate": "Bearer"},
 
    )
 
    try:
 
        payload = verify_access_token(token)
 
        user_id: str = payload.get("sub")
 
        if user_id is None:
 
            raise credentials_exception
 
    except JWTError:
 
        raise credentials_exception
 
    return {"user_id": int(user_id)}
 
# 在路由中使用
 
@app.get("/me")
 
async def read_self(current_user: dict = Depends(get_current_user)):
 
    return {"user_id": current_user["user_id"]}
 

flowchart TB

    A["请求: GET /me<br/>Header: Authorization: Bearer ey..."]

    A --> B["OAuth2PasswordBearer<br/>从请求头拔 token 字符串"]

    B --> C["get_current_user(token=token)"]

    C --> D["verify_access_token(token) → payload"]

    D --> E{"签名通过?"}

    E -->|"是"| F["继续"]

    E -->|"过期/伪造"| G["raise 401"]

    C --> H["return {'user_id': 37}"]

    F --> H

    H --> I["read_self(current_user={'user_id': 37})<br/>→ 返回 {'user_id': 37}"]

OAuth2PasswordRequestForm

登录接口接收的是 form-data 格式(x-www-form-urlencoded),不是 JSON。这是 FastAPI OAuth2 的默认规范。


第四步:注册 + 登录 + 刷新 — 完整路由

 
from fastapi import FastAPI, Depends, HTTPException, status
 
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
 
from pydantic import BaseModel
 
from datetime import timedelta
 
app = FastAPI(title="JWT 鉴权 Demo")
 
# ============================================================
 
fake_users_db: dict[str, dict] = {}
 
# ============================================================
 
class UserRegister(BaseModel):
 
    username: str
 
    password: str
 
class UserOut(BaseModel):
 
    id: int
 
    username: str
 
class TokenResponse(BaseModel):
 
    access_token: str
 
    refresh_token: str
 
    token_type: str = "bearer"
 
class RefreshRequest(BaseModel):
 
    refresh_token: str
 
# ============================================================
 
@app.post("/auth/register", response_model=UserOut)
 
async def register(data: UserRegister):
 
    if data.username in fake_users_db:
 
        raise HTTPException(status_code=400, detail="用户名已存在")
 
    hashed_password = pwd_context.hash(data.password)
 
    user_id = len(fake_users_db) + 1
 
    fake_users_db[data.username] = {
 
        "id": user_id,
 
        "username": data.username,
 
        "hashed_password": hashed_password,
 
    }
 
    return {"id": user_id, "username": data.username}
 
# ============================================================
 
@app.post("/auth/login", response_model=TokenResponse)
 
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
 
    user = fake_users_db.get(form_data.username)
 
    if not user:
 
        raise HTTPException(status_code=401, detail="用户名或密码错误")
 
    if not pwd_context.verify(form_data.password, user["hashed_password"]):
 
        raise HTTPException(status_code=401, detail="用户名或密码错误")
 
    access_token = create_access_token(data={"sub": str(user["id"])})
 
    refresh_token = create_refresh_token(data={"sub": str(user["id"])})
 
    return {
 
        "access_token": access_token,
 
        "refresh_token": refresh_token,
 
        "token_type": "bearer",
 
    }
 
# ============================================================
 
@app.post("/auth/refresh", response_model=TokenResponse)
 
async def refresh_access_token(data: RefreshRequest):
 
    try:
 
        payload = verify_access_token(data.refresh_token)
 
    except JWTError:
 
        raise HTTPException(status_code=401, detail="Refresh Token 无效或过期")
 
    if payload.get("type") != "refresh":
 
        raise HTTPException(status_code=401, detail="非法 Token 类型")
 
    user_id = payload.get("sub")
 
    new_access = create_access_token(data={"sub": user_id})
 
    new_refresh = create_refresh_token(data={"sub": user_id})
 
    return {
 
        "access_token": new_access,
 
        "refresh_token": new_refresh,
 
        "token_type": "bearer",
 
    }
 
# ============================================================
 
@app.get("/me", response_model=UserOut)
 
async def read_self(current_user: dict = Depends(get_current_user)):
 
    user_id = current_user["user_id"]
 
    for username, user in fake_users_db.items():
 
        if user["id"] == user_id:
 
            return {"id": user["id"], "username": user["username"]}
 
    raise HTTPException(status_code=404, detail="用户不存在")
 

完整鉴权链路


sequenceDiagram

    participant 浏览器

    participant FastAPI

    Note over 浏览器,FastAPI: 注册

    浏览器->>FastAPI: POST /auth/register {username, password}

    FastAPI->>FastAPI: 检查用户名唯一性

    FastAPI->>FastAPI: passlib.hash(password) -> 存哈希

    FastAPI-->>浏览器: 返回 {"id":1, "username":"alice"}

    Note over 浏览器,FastAPI: 登录

    浏览器->>FastAPI: POST /auth/login (form-data)

    FastAPI->>FastAPI: 查用户 -> passlib.verify(明文, 哈希) -> 匹配

    FastAPI->>FastAPI: 签发 Access Token (30min) + Refresh Token (7天)

    FastAPI-->>浏览器: 返回 Tokens

    Note over 浏览器,FastAPI: 请求受保护接口

    浏览器->>FastAPI: GET /me, Authorization: Bearer 〈access_token〉

    FastAPI->>FastAPI: OAuth2PasswordBearer 拔 token

    FastAPI->>FastAPI: get_current_user -> verify_access_token

    FastAPI->>FastAPI: jwt.decode -> 检查 exp -> 检查签名

    FastAPI-->>浏览器: 返回用户数据

    Note over 浏览器,FastAPI: Access Token 过期

    浏览器->>FastAPI: POST /auth/refresh (带 Refresh Token)

    FastAPI->>FastAPI: 验证签名 + 类型

    FastAPI->>FastAPI: 签发新 Access + Refresh Token(滚动刷新)

    FastAPI-->>浏览器: 新 Tokens

    浏览器->>浏览器: 更新存储,重试原请求


速查

JWT 最小骨架

 
# 配置
 
SECRET_KEY = os.environ["JWT_SECRET"]
 
ALGORITHM = "HS256"
 
# 签发
 
def create_token(user_id: int, minutes: int = 30) -> str:
 
    expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
 
    return jwt.encode(
 
        {"sub": str(user_id), "exp": expire, "iat": datetime.now(timezone.utc)},
 
        SECRET_KEY, algorithm=ALGORITHM
 
    )
 
# 验证
 
def verify_token(token: str) -> dict:
 
    return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
 
# FastAPI 依赖注入
 
oauth2 = OAuth2PasswordBearer(tokenUrl="/auth/login")
 
async def get_user(token: str = Depends(oauth2)) -> dict:
 
    try:
 
        return verify_token(token)
 
    except JWTError:
 
        raise HTTPException(401, "无效凭据")
 
@app.get("/me")
 
async def me(user: dict = Depends(get_user)):
 
    return user
 

密码处理

 
from passlib.context import CryptContext
 
pwd = CryptContext(schemes=["bcrypt"])
 
hashed = pwd.hash("mypassword")            # 注册
 
valid = pwd.verify("mypassword", hashed)   # 登录
 

速记卡(面试闪卡)

Q1:一句话讲清「FastAPI + JWT 全链路实现」到底是什么?

A:用 FastAPI 实现注册、登录签发与验证 JWT 的鉴权链路。

Q2:第一步:密码哈希 — 永远不存明文 —— 怎么理解?

A:像把密码锁进保险箱:用 bcrypt 哈希后只存密文,验证时内部取盐重算比对。bcrypt 带随机盐+2¹² 轮迭代,抗彩虹表、故意慢。

Q3:第二步:JWT 签发 + 验证 —— 怎么理解?

A:像发临时门禁卡:create_access_token 用 secret 把 payload 签成 header.payload.signature;verify 时重算签名比对,过期/伪造抛 JWTError。

Q4:第三步:FastAPI 依赖注入 —— 怎么理解?

A:用 OAuth2PasswordBearer 从 Authorization 头拔 token,Depends(get_current_user) 里 verify 后返 user_id;失败抛 401。这是 Dependency Injection(依赖注入)。

Q5:第四步:注册 + 登录 + 刷新 — 完整路由 —— 怎么理解?

A:注册存 bcrypt 哈希;登录验证后发 Access(30min)+Refresh(7天) 双 Token;/auth/refresh 验签名+类型滚动换新,登录用 form-data。

Q6:核心速记主线有哪些?

  • 密码永存明文,bcrypt 哈希+随机盐+慢迭代

  • JWT = header.payload.signature,靠 secret 签名防篡改

  • OAuth2PasswordBearer 拔 token,Depends 注入当前用户

  • 双 Token:Access 短、Refresh 长,刷新滚动换新

口诀

A:密码不存明

bcrypt上锁

JWT门禁卡

依赖注入活

相关链接


技术学习路线图 > JWT 鉴权