FastAPI 学习教程 · 第6部分

认证与授权(JWT + OAuth2)

💡 本部分目标:实现用户登录(返回 JWT Token),并保护 API 路由(只有携带有效 Token 的用户才能访问)。


✅ 一、为什么需要认证?

你的博客 API 目前是"公开"的------任何人都能创建、删除文章。

真实应用中,只有登录用户才能操作自己的内容

我们将使用:

  • OAuth2 密码流(Password Flow):适合第一方客户端(如 Web 前端)
  • JWT(JSON Web Token):轻量、无状态的令牌格式

🔒 认证(Authentication):你是谁?

🔐 授权(Authorization):你能做什么?


✅ 二、安装依赖

在虚拟环境中运行:

bash 复制代码
pip install python-jose[cryptography] passlib[bcrypt]
  • python-jose:用于生成和验证 JWT
  • passlib:用于安全地哈希密码(使用 bcrypt 算法)

✅ 三、步骤1:准备用户模型和假数据

3.1 更新 models.py

python 复制代码
# models.py
from sqlmodel import SQLModel, Field
from typing import Optional

class Post(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    content: str
    author: str
    published: bool = False

# 新增:用户模型(仅用于认证,不存真实密码)
class User(SQLModel):
    username: str
    email: str
    full_name: str | None = None

# 用于注册/登录的模型(含密码)
class UserInDB(User):
    hashed_password: str

⚠️ 注意:我们不会在数据库中存储明文密码,只存哈希值。

3.2 创建假用户数据(模拟数据库)

python 复制代码
# fake_db.py
from passlib.context import CryptContext
from models import UserInDB

# 密码哈希上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# 假用户数据库(实际项目用真实数据库)
fake_users_db = {
    "alice": {
        "username": "alice",
        "email": "alice@example.com",
        "full_name": "Alice W",
        "hashed_password": pwd_context.hash("secret123"),
        "disabled": False,
    },
    "bob": {
        "username": "bob",
        "email": "bob@example.com",
        "full_name": "Bob J",
        "hashed_password": pwd_context.hash("password456"),
        "disabled": False,
    }
}

def get_user(username: str):
    if username in fake_users_db:
        user_dict = fake_users_db[username]
        return UserInDB(**user_dict)

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def authenticate_user(username: str, password: str):
    user = get_user(username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user

✅ 四、步骤2:生成和验证 JWT Token

4.1 配置密钥和算法

python 复制代码
# security.py
from datetime import datetime, timedelta
from jose import jwt, JWTError
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from fake_db import get_user

# 密钥(生产环境应使用环境变量!)
SECRET_KEY = "your-super-secret-jwt-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# OAuth2 方案(用于 Swagger UI 的登录按钮)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="无法验证凭据",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    user = get_user(username=username)
    if user is None:
        raise credentials_exception
    return user

🔑 关键点:

  • oauth2_scheme 会自动在 Swagger 中添加"Authorize"按钮
  • get_current_user 是一个依赖,用于保护路由

✅ 五、步骤3:实现登录接口(/token)

python 复制代码
# main.py(新增导入)
from datetime import timedelta
from fastapi.security import OAuth2PasswordRequestForm
from fake_db import authenticate_user
from security import create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES

@app.post("/token")
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="用户名或密码错误",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}

💡 OAuth2PasswordRequestForm 自动解析表单格式的用户名/密码(Swagger 支持)


✅ 六、步骤4:保护你的 API 路由

现在,只有登录用户才能创建文章。

修改创建文章接口

python 复制代码
# main.py
from models import Post, User
from security import get_current_user

@app.post("/posts/", response_model=Post, status_code=201)
def create_post(
    post: Post,
    current_user: User = Depends(get_current_user),  # ← 添加这行
    session: Session = Depends(get_session)
):
    # 自动设置作者为当前用户
    post.author = current_user.username
    session.add(post)
    session.commit()
    session.refresh(post)
    return post

✅ 效果:

  • 请求头必须包含:Authorization: Bearer <your-token>
  • 否则返回 401 Unauthorized

✅ 七、完整项目结构更新

复制代码
blog-api/
├── main.py
├── models.py
├── fake_db.py      ← 新增
├── security.py     ← 新增
└── blog.db

✅ 八、测试流程(在 Swagger UI 中)

  1. 访问 http://127.0.0.1:8000/docs
  2. 点击右上角 "Authorize"
  3. 输入:
    • Username: alice
    • Password: secret123
  4. 点击 "Authorize" → 获取 Token
  5. 现在可以调用 /posts/ 创建文章!

🔍 创建的文章 author 字段会自动设为 alice


✅ 九、完整示例代码片段(关键文件)

security.py(完整)

python 复制代码
from datetime import datetime, timedelta
from jose import jwt, JWTError
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from fake_db import get_user

SECRET_KEY = "your-super-secret-jwt-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="无法验证凭据",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    user = get_user(username)
    if user is None:
        raise credentials_exception
    return user

✅ 十、练习任务(动手实践)

🧠 请先自己尝试完成,再查看下方答案!

任务1:保护所有写操作

  • 确保 /posts/{id} 的 PUT 和 DELETE 也要求登录
  • 只允许作者删除/更新自己的文章(提示:比较 post.authorcurrent_user.username

任务2:获取当前用户信息

  • 新增路由 GET /users/me
  • 返回当前登录用户信息(不含密码)
  • 使用 get_current_user 依赖

任务3(挑战):刷新 Token(可选)

  • 添加字段 refresh_token 到登录响应
  • (提示:本任务较难,可跳过,重点理解基础认证)

✅ 十一、练习任务参考答案

✅ 任务1 答案

更新 PUT 和 DELETE 路由:

python 复制代码
@app.put("/posts/{post_id}", response_model=Post)
def update_post(
    post_id: int,
    post_update: Post,
    current_user: User = Depends(get_current_user),
    session: Session = Depends(get_session)
):
    post = session.get(Post, post_id)
    if not post:
        raise HTTPException(status_code=404, detail="文章未找到")
    if post.author != current_user.username:
        raise HTTPException(status_code=403, detail="无权修改此文章")
    
    post_data = post_update.dict(exclude_unset=True)
    for key, value in post_data.items():
        setattr(post, key, value)
    session.add(post)
    session.commit()
    session.refresh(post)
    return post

@app.delete("/posts/{post_id}", status_code=204)
def delete_post(
    post_id: int,
    current_user: User = Depends(get_current_user),
    session: Session = Depends(get_session)
):
    post = session.get(Post, post_id)
    if not post:
        raise HTTPException(status_code=404, detail="文章未找到")
    if post.author != current_user.username:
        raise HTTPException(status_code=403, detail="无权删除此文章")
    session.delete(post)
    session.commit()

✅ 任务2 答案

python 复制代码
@app.get("/users/me", response_model=User)
def read_users_me(current_user: User = Depends(get_current_user)):
    return current_user

✅ 任务3 说明

刷新 Token 涉及更复杂的逻辑(如存储 refresh token、设置更长有效期等),初学者可暂不实现

重点掌握基础 JWT 认证即可。


✅ 十二、小结

在本部分,你学会了:

  • 使用 OAuth2 密码流 实现登录
  • 生成和验证 JWT Token
  • Depends(get_current_user) 保护路由
  • 在 Swagger UI 中测试带认证的 API
  • 实现 基于角色的权限控制(作者只能操作自己的文章)

你的博客 API 现在已经具备完整的用户认证系统

相关推荐
风送雨2 小时前
FastAPI 学习教程 · 第5部分
jvm·学习·fastapi
副露のmagic2 小时前
更弱智的算法学习 day48
学习·算法
Nan_Shu_6142 小时前
学习: Threejs (15)& Threejs (16)
学习·three.js
知识分享小能手2 小时前
Oracle 19c入门学习教程,从入门到精通,Oracle 过程、函数、触发器和包详解(7)
数据库·学习·oracle
漏刻有时2 小时前
微信小程序学习实录14:微信小程序手写签名功能完整开发方案
学习·微信小程序·notepad++
魔芋红茶3 小时前
Spring Security 学习笔记 1:快速开始
笔记·学习·spring
皮蛋sol周3 小时前
嵌入式学习数据结构(三)栈 链式 循环队列
arm开发·数据结构·学习·算法··循环队列·链式队列
Kratzdisteln3 小时前
【1902】优化后的三路径学习系统
android·学习
仰泳之鹅3 小时前
【PID学习】多环PID
学习·pid