昨天搞定了异步优化,今天来解决一些实际问题。Day4的API虽然性能不错,但还缺少一些企业级应用必备的功能。
现在的问题
- 前端无法访问API(跨域问题)
- 没有请求日志,出问题难以排查
- 错误信息格式不统一
- 缺少统一的请求处理机制
解决思路
用中间件来解决这些问题。中间件就像给API加上"门卫",每个请求都要经过这些门卫的检查和处理。
分三步走:
- CORS中间件 - 解决跨域问题
- 日志中间件 - 记录请求信息
- 异常处理器 - 统一错误格式
步骤1:CORS中间件
什么是CORS?
CORS(跨域资源共享)是浏览器的安全机制。默认情况下,浏览器只允许同一个域名下的网页访问API。
开发时经常遇到这个问题:
- 前端运行在
http://localhost:3000
- 后端运行在
http://localhost:8000
这就是跨域访问,浏览器会直接阻止。CORS中间件就是告诉浏览器哪些外部地址可以访问我们的API。
添加CORS中间件
先解决最常见的跨域问题:
# v5_middleware/main.py
"""
博客系统v5.0 - 中间件版本
添加CORS、日志等中间件支持
"""from fastapi import FastAPI, HTTPException, Depends, status
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional
import logging# 导入Day4的模块
import crud
from database import get_async_db, create_tables
from schemas import UserRegister, UserResponse, UserLogin, PostCreate, PostResponse# 配置日志
logging.basicConfig(level=logging.INFO,format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)app = FastAPI(title="博客系统API v5.0",description="7天FastAPI学习系列 - Day5中间件版本",version="5.0.0"
)# 添加CORS中间件 - 解决前端跨域问题
app.add_middleware(CORSMiddleware,allow_origins=["http://localhost:3000", # React开发服务器"http://127.0.0.1:3000", # 本地访问"http://localhost:5173", # Vite开发服务器"http://127.0.0.1:5173" # Vite本地访问],allow_credentials=True, # 允许携带认证信息(cookies等)allow_methods=["*"], # 允许所有HTTP方法allow_headers=["*"], # 允许所有请求头
)logger.info("CORS中间件已配置,支持前端跨域访问")# 全局变量:当前用户(Day7会用JWT替换)
current_user_id: Optional[int] = None# 应用启动时创建数据表
@app.on_event("startup")
async def startup_event():"""应用启动时异步创建数据表"""await create_tables()logger.info("数据库表创建完成")
现在前端就可以正常访问我们的API了。
测试CORS效果
使用curl命令来测试CORS配置是否正确:
1. 测试基本API连接
# 测试根路由
curl -H "Origin: http://localhost:3000" -v http://localhost:8000/# 预期响应头应包含:
# Access-Control-Allow-Origin: http://localhost:3000
# Access-Control-Allow-Credentials: true
2. 测试预检请求(OPTIONS)
# 测试POST请求的预检
curl -H "Origin: http://localhost:3000" \-H "Access-Control-Request-Method: POST" \-H "Access-Control-Request-Headers: Content-Type" \-X OPTIONS -v http://localhost:8000/users/register# 预期响应头应包含:
# Access-Control-Allow-Origin: http://localhost:3000
# Access-Control-Allow-Methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT
# Access-Control-Allow-Headers: accept, accept-encoding, authorization, content-type, dnt, origin, user-agent, x-csrftoken, x-requested-with
3. 测试用户注册(跨域POST请求)
# 测试用户注册
curl -H "Origin: http://localhost:3000" \-H "Content-Type: application/json" \-X POST \-d '{"username": "红发香克斯", "email": "xiangkesi@example.com", "password": "TestPass136!"}' \-v http://localhost:8000/users/register# 成功响应示例:
# {
# "id": 1,
# "username": "红发香克斯",
# "email": "xiangkesi@example.com",
# "created_at": "2025-08-26T10:00:00"
# }
4. 测试用户登录(跨域POST请求)
# 测试用户登录
curl -H "Origin: http://localhost:3000" \-H "Content-Type: application/json" \-X POST \-d '{"account":"红发香克斯","password": "TestPass136!"}' \-v http://localhost:8000/users/login# 成功响应示例:
# {
# "message": "登录成功",
# "user": {
# "id": 1,
# "username": "红发香克斯",
# "email": "xiangkesi@example.com",
# "created_at": "2025-08-26T10:02:00"
# }
# }
5. 关键CORS响应头说明
在curl的-v
输出中,注意观察这些响应头:
- Access-Control-Allow-Origin: 允许访问的源地址
- Access-Control-Allow-Methods: 允许的HTTP方法
- Access-Control-Allow-Headers: 允许的请求头
- Access-Control-Allow-Credentials: 是否允许携带认证信息
6. 测试不同源的访问
# 测试未配置的源(应该被拒绝)
curl -H "Origin: http://evil-site.com" -v http://localhost:8000/# 测试配置的源(应该被允许)
curl -H "Origin: http://localhost:5173" -v http://localhost:8000/
如果CORS配置正确,你应该看到:
- 配置的源返回相应的
Access-Control-Allow-Origin
头 - 未配置的源不会返回CORS相关头部
- 所有跨域请求都能正常处理
步骤2:日志中间件
为什么需要日志?
日志在API开发中很重要,可以帮我们:
- 排查问题 - 出错时知道是哪个请求出的问题
- 性能监控 - 哪些API响应慢,需要优化
- 用户行为分析 - 哪些功能使用频率高
- 安全监控 - 发现异常的访问模式
添加请求日志中间件
# 继续在main.py中添加
import time
from fastapi import Request@app.middleware("http")
async def log_requests(request: Request, call_next):"""请求日志中间件记录每个请求的详细信息和处理时间"""start_time = time.time()# 记录请求开始logger.info("请求开始: %s %s - 客户端: %s",request.method, request.url, request.client.host if request.client else 'unknown')# 处理请求response = await call_next(request)# 计算处理时间process_time = time.time() - start_time# 记录请求结束status_text = "成功" if response.status_code < 400 else "失败"logger.info("请求完成(%s): %s %s - 状态码: %d - 耗时: %.4f秒",status_text,request.method, request.url, response.status_code, process_time)# 在响应头中添加处理时间(方便前端监控)response.headers["X-Process-Time"] = str(process_time)# 如果响应时间过长,记录警告if process_time > 1:logger.warning("慢请求警告: %s %s 耗时 %.4f秒,建议优化",request.method, request.url, process_time)return response
添加更详细的日志记录
为特定的操作添加更详细的日志:
# 在API函数中添加业务日志
@app.post("/users/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserRegister, db: AsyncSession = Depends(get_async_db)):"""用户注册 - 添加详细日志"""logger.info(f"用户注册请求: 用户名={user_data.username}, 邮箱={user_data.email}")try:db_user = await crud.create_user(db, username=user_data.username,email=user_data.email,password=user_data.password)logger.info(f"用户注册成功: ID={db_user.id}, 用户名={db_user.username}")return UserResponse(id=db_user.id,username=db_user.username,email=db_user.email,created_at=db_user.created_at)except ValueError as e:logger.warning(f"用户注册失败: {str(e)} - 用户名={user_data.username}")raise HTTPException(status_code=400, detail=str(e))except Exception as e:logger.error(f"用户注册异常: {str(e)} - 用户名={user_data.username}")raise HTTPException(status_code=500, detail=f"创建用户失败: {str(e)}")@app.post("/users/login")
async def login_user(login_data: UserLogin, db: AsyncSession = Depends(get_async_db)):"""用户登录 - 添加详细日志"""logger.info(f"用户登录请求: 账号={login_data.account}")global current_user_iduser = 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="用户名或密码错误")current_user_id = user.idlogger.info(f"用户登录成功: ID={user.id}, 用户名={user.username}")return {"message": "登录成功","user": UserResponse(id=user.id,username=user.username,email=user.email,created_at=user.created_at)}
现在启动服务器,你会看到详细的日志输出:
uvicorn main:app --reload --host 0.0.0.0 --port 8000
控制台输出类似:
INFO: Started reloader process [21957] using WatchFiles
2025-08-26 17:43:00,350 - main - INFO - CORS中间件已配置,支持前端跨域访问
INFO: Started server process [21959]
INFO: Waiting for application startup.
2025-08-26 17:43:00,369 - main - INFO - 数据库表创建完成
INFO: Application startup complete.
2025-08-26 17:43:26,120 - main - INFO - 请求开始: POST http://localhost:8000/users/login - 客户端: 127.0.0.1
2025-08-26 17:43:26,122 - main - INFO - 用户登录请求: 账户=洛克斯
2025-08-26 17:43:26,131 - main - INFO - 用户登录成功: ID=5, 用户名=洛克斯
2025-08-26 17:43:26,131 - main - INFO - 请求完成(成功): POST http://localhost:8000/users/login - 状态码: 200 - 耗时: 0.0117秒
INFO: 127.0.0.1:48842 - "POST /users/login HTTP/1.1" 200 OK
步骤3:异常处理器
为什么需要统一异常处理?
Day4中的错误处理比较简单,不同的错误可能返回不同格式的信息。统一异常处理可以让所有错误都有标准的格式和处理方式。
注意:异常处理器不是中间件,它们是FastAPI的异常处理机制,只在发生异常时才会被触发。
添加全局异常处理器
# 在main.py中添加异常处理
from fastapi import Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):"""HTTP异常处理器统一处理所有HTTP异常,返回标准格式"""logger.error("HTTP异常: %d - %s - 请求: %s %s",exc.status_code, exc.detail, request.method, request.url )return JSONResponse(status_code=exc.status_code,content={"error": True,"status_code": exc.status_code,"message": exc.detail,"path": str(request.url),"timestamp": time.time()})@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):"""数据验证异常处理器处理Pydantic模型验证错误"""error_messages = [error['msg'] for error in exc.errors()]logger.warning("数据验证失败: %s - 请求: %s %s",error_messages,request.method, request.url)return JSONResponse(status_code=422,content={"error": True,"status_code": 422,"message": "数据验证失败","details": exc.errors(),"path": str(request.url),"timestamp": time.time()})@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):"""通用异常处理器处理所有未捕获的异常"""logger.error("未处理异常:%s: %s - 请求:%s %s",type(exc).__name__, str(exc), request.method, request.url,exc_info=True )return JSONResponse(status_code=500,content={"error": True,"status_code": 500,"message": "服务器内部错误","path": str(request.url),"timestamp": time.time()})
添加健康检查和根路由
完善一下基础路由,并添加健康检查:
# ===== 根路由 =====@app.get("/")
async def root():"""欢迎页面"""logger.info("访问根路由")return {"message": "欢迎使用博客系统API v5.0","version": "5.0.0","docs": "/docs","features": ["用户管理", "文章管理", "数据验证增强", "数据库持久化", "异步优化", "CORS支持","请求日志","异常处理"],"next_version": "Day6将添加依赖注入"}@app.get("/health")
async def health_check(db: AsyncSession = Depends(get_async_db)):"""健康检查接口"""try:# 检查数据库连接user_count = await crud.get_user_count(db)post_count = await crud.get_post_count(db)logger.info(f"健康检查通过: 用户数={user_count}, 文章数={post_count}")return {"status": "healthy","version": "5.0.0","users_count": user_count,"posts_count": post_count,"database": "SQLite with async support","middleware": "CORS、日志、异常处理","performance": "异步优化已启用"}except Exception as e:logger.error(f"健康检查失败: {str(e)}")raise HTTPException(status_code=503, detail="服务不可用")
测试异常处理效果
测试一下异常处理是否正常工作:
# 1. 测试正常请求
curl http://localhost:8000/# 2. 测试404错误
curl http://localhost:8000/none# 3. 测试数据验证错误
curl -X POST "http://localhost:8000/users/register" \-H "Content-Type: application/json" \-d '{"username": "","email": "dd-email","password": "123"}'# 4. 测试健康检查
curl http://localhost:8000/health
现在所有的错误都会返回统一格式的JSON响应,并且在日志中记录详细信息。
今日总结
完成了两个重要的中间件和一套异常处理器:
- CORS中间件 - 解决前端跨域访问问题
- 请求日志中间件 - 记录所有API请求和响应时间
- 异常处理器 - 统一错误响应格式
Day4 vs Day5 对比
方面 | Day4 | Day5 |
---|---|---|
跨域支持 | 无,前端无法访问 | CORS中间件,完美支持 |
请求日志 | 无 | 详细的请求日志和性能监控 |
错误处理 | 格式不统一 | 统一的错误响应格式 |
问题排查 | 困难 | 有详细日志,容易排查 |
前端对接 | 无法对接 | 可以正常对接 |
中间件执行顺序
FastAPI中的中间件执行遵循洋葱模型(Onion Model):
- 请求阶段:中间件按照添加的顺序执行
- 响应阶段:中间件按照添加的相反顺序执行
- 对于我们的两个中间件:
- CORS中间件:先添加,在请求阶段先执行,在响应阶段后执行(内层)
- 日志中间件:后添加,在请求阶段后执行,在响应阶段先执行(外层)
注意:异常处理器不是中间件,它们独立于中间件执行顺序,只在异常发生时触发。
推荐的添加顺序
# 1. 先添加CORS中间件(在请求阶段先执行)
app.add_middleware(CORSMiddleware, ...)# 2. 再添加日志中间件(在请求阶段后执行)
@app.middleware("http")
async def log_requests(...):# 3. 异常处理器(独立执行)
@app.exception_handler(...)
这样安排的好处:
- 日志中间件先处理请求,能记录包括CORS处理在内的完整请求信息
- 日志中间件后处理请求,记录响应信息,然后CORS中间件处理响应头
- 异常处理器独立工作,统一处理所有异常
明天学习依赖注入系统,让代码更简洁和可维护。