昨天搞定了異步優化,今天來解決一些實際問題。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中間件處理響應頭
- 異常處理器獨立工作,統一處理所有異常
明天學習依賴注入系統,讓代碼更簡潔和可維護。