Day5-中间件与请求处理

昨天搞定了异步优化,今天来解决一些实际问题。Day4的API虽然性能不错,但还缺少一些企业级应用必备的功能。

现在的问题

  • 前端无法访问API(跨域问题)
  • 没有请求日志,出问题难以排查
  • 错误信息格式不统一
  • 缺少统一的请求处理机制

解决思路

用中间件来解决这些问题。中间件就像给API加上"门卫",每个请求都要经过这些门卫的检查和处理。

分三步走:

  1. CORS中间件 - 解决跨域问题
  2. 日志中间件 - 记录请求信息
  3. 异常处理器 - 统一错误格式

步骤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响应,并且在日志中记录详细信息。

今日总结

完成了两个重要的中间件和一套异常处理器:

  1. CORS中间件 - 解决前端跨域访问问题
  2. 请求日志中间件 - 记录所有API请求和响应时间
  3. 异常处理器 - 统一错误响应格式

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中间件处理响应头
  • 异常处理器独立工作,统一处理所有异常

明天学习依赖注入系统,让代码更简洁和可维护。

相关推荐
只因在人海中多看了你一眼13 小时前
B.50.10.01-消息队列与电商应用
中间件
子非鱼@Itfuture20 小时前
【Kafka】Kafka使用场景用例&Kafka用例图
分布式·中间件·kafka
叫我阿柒啊1 天前
从Java全栈到前端框架:一位程序员的实战之路
java·spring boot·微服务·消息队列·vue3·前端开发·后端开发
中国胖子风清扬1 天前
Rust 序列化技术全解析:从基础到实战
开发语言·c++·spring boot·vscode·后端·中间件·rust
蓝倾1 天前
京东商品属性API数据解析:颜色、尺寸与材质
api·fastapi
Lilixxs1 天前
VBA 中使用 ADODB 操作 SQLite 插入中文乱码问题
数据库·中间件·sqlite·乱码·vba·odbc·adodb
CodeDevMaster2 天前
使用Transformers、ChatGLM3项目、创建FastAPI应用等方式部署调用ChatGLM3-6B模型
llm·fastapi·chatglm (智谱)
Ray Song3 天前
【FastDDS】Layer DDS之Domain (01-overview)
中间件·自动驾驶·fastdds·dds层概览
Thuni_soft3 天前
权威认证!华宇TAS应用中间件获得商用密码产品认证证书
中间件·商用密码认证