昨天搞定了异步优化,今天来解决一些实际问题。Day4的API虽然性能不错,但还缺少一些企业级应用必备的功能。
现在的问题
- 前端无法访问API(跨域问题)
- 没有请求日志,出问题难以排查
- 错误信息格式不统一
- 缺少统一的请求处理机制
解决思路
用中间件来解决这些问题。中间件就像给API加上"门卫",每个请求都要经过这些门卫的检查和处理。
分三步走:
- CORS中间件 - 解决跨域问题
- 日志中间件 - 记录请求信息
- 异常处理器 - 统一错误格式
步骤1:CORS中间件
什么是CORS?
CORS(跨域资源共享)是浏览器的安全机制。默认情况下,浏览器只允许同一个域名下的网页访问API。
开发时经常遇到这个问题:
- 前端运行在
http://localhost:3000
- 后端运行在
http://localhost:8000
这就是跨域访问,浏览器会直接阻止。CORS中间件就是告诉浏览器哪些外部地址可以访问我们的API。
添加CORS中间件
先解决最常见的跨域问题:
python
# 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连接
bash
# 测试根路由
curl -H "Origin: http://localhost:3000" -v http://localhost:8000/
# 预期响应头应包含:
# Access-Control-Allow-Origin: http://localhost:3000
# Access-Control-Allow-Credentials: true
2. 测试预检请求(OPTIONS)
bash
# 测试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请求)
bash
# 测试用户注册
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请求)
bash
# 测试用户登录
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. 测试不同源的访问
bash
# 测试未配置的源(应该被拒绝)
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响应慢,需要优化
- 用户行为分析 - 哪些功能使用频率高
- 安全监控 - 发现异常的访问模式
添加请求日志中间件
python
# 继续在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
添加更详细的日志记录
为特定的操作添加更详细的日志:
python
# 在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_id
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="用户名或密码错误")
current_user_id = user.id
logger.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
)
}
现在启动服务器,你会看到详细的日志输出:
bash
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的异常处理机制,只在发生异常时才会被触发。
添加全局异常处理器
python
# 在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()
}
)
添加健康检查和根路由
完善一下基础路由,并添加健康检查:
python
# ===== 根路由 =====
@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="服务不可用")
测试异常处理效果
测试一下异常处理是否正常工作:
bash
# 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中间件:先添加,在请求阶段先执行,在响应阶段后执行(内层)
- 日志中间件:后添加,在请求阶段后执行,在响应阶段先执行(外层)
注意:异常处理器不是中间件,它们独立于中间件执行顺序,只在异常发生时触发。
推荐的添加顺序
python
# 1. 先添加CORS中间件(在请求阶段先执行)
app.add_middleware(CORSMiddleware, ...)
# 2. 再添加日志中间件(在请求阶段后执行)
@app.middleware("http")
async def log_requests(...):
# 3. 异常处理器(独立执行)
@app.exception_handler(...)
这样安排的好处:
- 日志中间件先处理请求,能记录包括CORS处理在内的完整请求信息
- 日志中间件后处理请求,记录响应信息,然后CORS中间件处理响应头
- 异常处理器独立工作,统一处理所有异常
明天学习依赖注入系统,让代码更简洁和可维护。