认证与授权(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:用于生成和验证 JWTpasslib:用于安全地哈希密码(使用 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 中)
- 访问 http://127.0.0.1:8000/docs
- 点击右上角 "Authorize"
- 输入:
- Username:
alice - Password:
secret123
- Username:
- 点击 "Authorize" → 获取 Token
- 现在可以调用
/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.author和current_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 现在已经具备完整的用户认证系统!