FastAPI 系列

  1. FastAPI 系列:从第一个接口到生产部署

  2. FastAPI 为什么适合做 JSON API

  3. 第一个接口怎么跑起来

  4. 路径、查询和请求体为什么要分开声明

  5. Pydantic 模型怎么当契约

  6. 路由怎么拆

  7. 依赖注入到底省了什么

  8. 数据库会话怎么进接口

  9. 状态码和异常为什么不要裸 raise

  10. 鉴权怎么接到 Depends 上(当前篇)

  11. 中间件、CORS 和后台任务分别解决什么

  12. 测试怎么写才不连真实库

  13. 生产怎么部署

鉴权不要散落在每个路由里拆 Header。FastAPI 的做法是:OAuth2PasswordBearer 取出 Bearer Token,get_current_user 校验并返回用户,需要登录的路径只声明这个依赖。下面只讲防御侧实现:密码哈希、短时 JWT、失败时的 401。

客户端用账号换 JWT,后续请求经 Bearer 依赖解码用户

密码怎么存

官方示例现在用 pwdlib 做哈希,不要自己写 SHA256 加盐。校验失败时仍应对「不存在的用户」走一遍哈希,避免用响应时间暴露账号是否存在。

from pwdlib import PasswordHash

password_hash = PasswordHash.recommended()


def verify_password(plain: str, hashed: str) -> bool:
    return password_hash.verify(plain, hashed)

签发短时 JWT

from datetime import datetime, timedelta, timezone

import jwt

SECRET_KEY = settings.secret_key  # 来自环境变量
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30


def create_access_token(subject: str) -> str:
    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    return jwt.encode({"sub": subject, "exp": expire}, SECRET_KEY, algorithm=ALGORITHM)

密钥用 openssl rand -hex 32 生成,放环境变量。过期时间写进 exp,校验时用库函数解码,不要自己解析 payload 字符串。

登录与受保护路由

from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt.exceptions import InvalidTokenError

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()


@app.post("/token")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return {"access_token": create_access_token(user.username), "token_type": "bearer"}


async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username = payload.get("sub")
        if username is None:
            raise credentials_exception
    except InvalidTokenError:
        raise credentials_exception
    user = get_user(username)
    if user is None:
        raise credentials_exception
    return user


@app.get("/users/me")
async def read_me(current_user: Annotated[User, Depends(get_current_user)]):
    return current_user

tokenUrl="token" 会让 Swagger UI 出现授权按钮,指向同一个登录接口。权限组合继续套依赖:get_current_active_user 检查 disabled,管理员再包一层。

常见误区

把 JWT 当会话存储

Access Token 应短时。需要吊销时,用服务端会话或黑名单,不要发一周有效的 HS256 Token 还不做刷新策略。

密钥写进仓库

示例里的 hex 字符串只能出现在文档,不能出现在真实部署。

在中间件里自己解析 Authorization

除非做网关。应用内用 OAuth2PasswordBearer,OpenAPI 才会正确标记安全方案。

小结

登录换短时 Token,受保护路由只依赖 get_current_user。密码哈希、过期、401 头缺一不可。下一篇分清中间件、CORS 和后台任务,避免把鉴权塞进错误的一层。

参考资料