昨天搞定了依赖注入的基础,今天来解决认证问题。Day6我们留下了了一个问题:用全局变量current_user_id
来记录登录状态,这在多进程环境下会有问题。今天用JWT来彻底解决这个问题。
现在的问题
Day6我们用全局变量current_user_id
来记录登录状态,还有很多重复的认证和权限检查代码。全局变量在多进程环境下会有问题,而且每个API都要重复写相同的检查逻辑。
解决思路
用JWT(JSON Web Token)替换全局变量。JWT的好处:
- 无状态:服务器不需要存储用户状态,所有信息都在token里
- 可验证:用密钥签名,防止篡改
- 跨服务:多个服务可以用同一个密钥验证token
然后用依赖注入把认证和权限检查抽出来,就像Day6的分页依赖一样。
我们分几步来改:
- 安装JWT库并配置密钥
- 创建JWT工具函数
- 实现认证与权限依赖
- 更新数据模型
- 更新登录API
- 更新文章API使用认证和权限依赖
- 测试JWT认证
第一步:安装JWT库
pip install python-jose[cryptography]
第二步:JWT配置和工具函数
创建auth.py
:
# v7_jwt/auth.py
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from fastapi import HTTPException, status
# JWT配置
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
"""创建JWT token"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_token(token: str) -> dict:
"""验证JWT token并返回payload"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id_str: str = payload.get("sub")
if user_id_str is None:
raise HTTPException(
status_code=401,
detail="无效的认证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
# 转换为整数并验证
try:
user_id = int(user_id_str)
except (ValueError, TypeError):
raise HTTPException(
status_code=401,
detail="无效的用户ID",
headers={"WWW-Authenticate": "Bearer"},
)
return {"user_id": user_id}
except JWTError:
raise HTTPException(
status_code=401,
detail="无效的认证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
第三步:认证与权限依赖
在dependencies.py
中添加认证和权限相关的依赖:
# v7_jwt/dependencies.py
from fastapi import Depends, HTTPException, status, Query
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from auth import verify_token
from database import AsyncSessionLocal
import crud
# 原有的依赖...
async def get_async_db():
"""数据库会话依赖"""
# ... 保持不变
def get_pagination(
page: int = Query(1, ge=1, description="页码,从1开始"),
size: int = Query(10, ge=1, le=100, description="每页数量,最大100")
):
"""分页参数依赖"""
# ... 保持不变
# 新增:认证和权限依赖
security = HTTPBearer()
# 认证依赖:解决"你是谁"的问题
async def get_current_user_id(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> int:
"""获取当前用户ID(认证依赖)"""
token = credentials.credentials
payload = verify_token(token)
return payload["user_id"]
async def get_current_user(
user_id: int = Depends(get_current_user_id),
db: AsyncSession = Depends(get_async_db)
):
"""获取当前用户对象(认证依赖)"""
user = await crud.get_user_by_id(db, user_id)
if not user:
raise HTTPException(
status_code=401,
detail="用户不存在"
)
return user
# 权限依赖:解决"你能做什么"的问题
async def verify_post_owner(
post_id: int,
current_user_id: int = Depends(get_current_user_id), # 先认证:从JWT获取用户ID
db: AsyncSession = Depends(get_async_db)
):
"""验证文章所有权(权限依赖)
自动检查:用户身份认证 → 文章存在性 → 所有权验证
成功时返回文章对象,失败时抛出HTTP异常(401/403/404)
"""
post = await crud.get_post_by_id(db, post_id)
if not post:
raise HTTPException(status_code=404, detail="文章不存在")
if post.author_id != current_user_id:
raise HTTPException(status_code=403, detail="只能操作自己的文章")
return post
第四步:更新数据模型
首先在 schemas.py
中添加认证相关的模型:
# v7_jwt/schemas.py
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
# 原有的模型保持不变
class TokenResponse(BaseModel):
access_token: str
token_type: str
expires_in: int
第五步:更新登录API
现在登录API要返回JWT token而不是设置全局变量:
# v7_jwt/main.py
from fastapi import FastAPI, HTTPException, Depends, status, Request,Query
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional
from datetime import timedelta
from dependencies import get_async_db, get_pagination, get_current_user, get_current_user_id, verify_post_owner
from schemas import UserRegister, UserResponse, UserLogin, PostCreate, PostResponse,TokenResponse
from auth import create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES
import crud
import models
app = FastAPI(
title="博客系统API v7.0",
description="7天FastAPI学习系列 - Day7JWT版本",
version="7.0.0"
)
# 改造登录API - 返回JWT token而不是设置全局变量
@app.post("/users/login", response_model=TokenResponse)
async def login_user(login_data: UserLogin, db: AsyncSession = Depends(get_async_db)):
"""用户登录"""
logger.info(f"用户登录请求: 账户={login_data.account}")
user = await crud.authenticate_user(db, login_data.account, login_data.password)
if not user:
logger.warning(f"登录失败: 账号或密码错误 - 账号={login_data.account}")
raise HTTPException(status_code=401, detail="用户名或密码错误")
logger.info(f"用户登录成功: ID={user.id}, 用户名={user.username}")
# Day7 新增:创建并返回JWT token
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": str(user.id)},
expires_delta=access_token_expires
)
return {
"access_token": access_token,
"token_type": "bearer",
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60
}
# 改造用户资料API - 使用JWT认证依赖
@app.get("/users/profile", response_model=UserResponse)
async def get_current_user_profile(
current_user = Depends(get_current_user)
):
"""获取当前登录用户信息(Day7 JWT认证版)"""
# Day6及之前:从全局变量获取用户
# Day7:通过JWT依赖自动获取认证用户
return UserResponse(
id=current_user.id,
username=current_user.username,
email=current_user.email,
created_at=current_user.created_at
)
第六步:更新文章API使用认证和权限依赖
现在所有需要登录的API都可以用依赖注入了:
# 创建文章 - 使用认证依赖
@app.post("/posts", response_model=PostResponse)
async def create_post(
post: PostCreate,
current_user_id: int = Depends(get_current_user_id), # 依赖注入获取用户ID
db: AsyncSession = Depends(get_async_db)
):
"""创建新文章"""
# 不用再写 if not current_user_id 了!
db_post = await crud.create_post(db, post, author_id=current_user_id)
return PostResponse(
id=db_post.id,
title=db_post.title,
content=db_post.content,
author_id=db_post.author_id,
created_at=db_post.created_at,
updated_at=db_post.updated_at
)
# 更新文章 - 使用权限依赖
@app.put("/posts/{post_id}", response_model=PostResponse)
async def update_post(
post_id: int, # 显式声明路径参数
post_data: PostCreate,
post = Depends(verify_post_owner), # 依赖注入验证权限并获取文章
db: AsyncSession = Depends(get_async_db)
):
"""更新文章"""
# 不用再写权限检查了!verify_post_owner已经帮我们做了
updated_post = await crud.update_post(db, post, post_data)
return PostResponse(
id=updated_post.id,
title=updated_post.title,
content=updated_post.content,
author_id=updated_post.author_id,
created_at=updated_post.created_at,
updated_at=updated_post.updated_at
)
# 删除文章 - 使用权限依赖
@app.delete("/posts/{post_id}")
async def delete_post_api(
post_id: int,
post = Depends(verify_post_owner),
db: AsyncSession = Depends(get_async_db)
):
"""删除文章"""
success = await crud.delete_post(db, post_id)
if not success:
raise HTTPException(status_code=500, detail="删除文章失败")
return {"message": "文章删除成功"}
第七步:测试JWT认证
1. 登录获取token
curl -X POST "http://localhost:8000/users/login" \
-H "Content-Type: application/json" \
-d '{"account": "loji@qq.com","password": "Test136!"}'
# 返回:
# {
# "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
# "token_type": "bearer",
# "expires_in": 1800
# }
2. 使用token访问需要认证的API
# 获取当前用户信息
curl "http://localhost:8000/users/profile" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# 创建文章
curl -X POST "http://localhost:8000/posts" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{"title": "海贼王1159话情报", "content": "洛克斯真名为“戴维•D•吉贝克”!夏琪是神之谷大赛的奖品"}'
3. 测试权限控制
# 尝试编辑别人的文章(应该返回403错误)
curl -X PUT "http://localhost:8000/posts/999" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{"title": "海贼王最终章", "content": "千两道化-巴基当上海贼王,世界迎来红鼻子时代!"}'
今日总结
- JWT工具函数 - 创建和验证token,替代全局变量
- 认证依赖 -
get_current_user_id
和get_current_user
解决"你是谁" - 权限依赖 -
verify_post_owner
解决"你能做什么"
其他需要认证或权限控制的接口也可以采用类似的依赖注入方式实现。
系列回顾
通过这7天的学习,我们从零开始构建了一个完整的博客系统。虽然项目简单,但是我觉得对于想快速入门FastAPI的同学来说,应该还有一定帮助的。
项目开源
代码已上传github上,这是github仓库地址,如果项目对你有帮助,欢迎点个Star鼓励!