FastAPI 请求验证:超越 Pydantic 基础,构建企业级验证体系
引言:为什么需要超越基础的请求验证?
在现代 API 开发中,请求验证远不止是检查数据类型是否正确。随着系统复杂性的增加,我们需要处理更复杂的验证场景:多字段关联验证、数据库一致性检查、业务规则验证、第三方服务集成验证等。FastAPI 基于 Pydantic 提供了出色的基础验证能力,但在实际企业应用中,我们需要构建更完整、更健壮的验证体系。
本文将深入探讨 FastAPI 请求验证的高级技巧和架构模式,帮助开发者构建可维护、可测试且安全的企业级验证系统。
一、Pydantic 验证的深度探索
1.1 自定义验证器的进阶用法
Pydantic 的 @validator 装饰器提供了字段级验证,但实际开发中,我们经常需要更复杂的验证逻辑。让我们看一个超越简单示例的复杂场景:
python
from pydantic import BaseModel, validator, Field, root_validator
from typing import Optional, List, Dict
from datetime import datetime, timedelta
import re
class AdvancedUserRegistration(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
email: str
password: str
password_confirmation: str
date_of_birth: datetime
referral_code: Optional[str] = None
subscription_plan: str = Field(default="basic")
custom_attributes: Dict[str, str] = {}
@validator('username')
def username_must_be_valid(cls, v):
# 检查用户名是否包含非法字符
if not re.match(r'^[a-zA-Z0-9_]+$', v):
raise ValueError('用户名只能包含字母、数字和下划线')
# 检查保留用户名
reserved_usernames = ['admin', 'system', 'root']
if v.lower() in reserved_usernames:
raise ValueError('该用户名已被保留')
return v
@validator('email')
def email_must_be_valid(cls, v):
# 简单的邮箱格式验证
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', v):
raise ValueError('邮箱格式无效')
# 检查邮箱域名是否在黑名单中
blacklisted_domains = ['tempmail.com', 'throwaway.com']
domain = v.split('@')[1]
if domain in blacklisted_domains:
raise ValueError('该邮箱域名不被接受')
return v
@validator('date_of_birth')
def must_be_adult(cls, v):
# 检查用户是否已满18岁
eighteen_years_ago = datetime.now() - timedelta(days=365*18)
if v > eighteen_years_ago:
raise ValueError('用户必须年满18岁')
return v
@validator('subscription_plan')
def validate_subscription_plan(cls, v):
valid_plans = ['basic', 'premium', 'enterprise']
if v not in valid_plans:
raise ValueError(f'订阅计划必须是以下之一: {", ".join(valid_plans)}')
# 如果选择企业版,需要额外验证
if v == 'enterprise':
# 这里可以添加企业版特定的验证逻辑
pass
return v
@root_validator
def validate_passwords_match(cls, values):
# 根验证器可以访问所有字段的值
if 'password' in values and 'password_confirmation' in values:
if values['password'] != values['password_confirmation']:
raise ValueError('密码和确认密码不匹配')
# 检查密码强度
password = values.get('password', '')
if len(password) < 8:
raise ValueError('密码长度至少为8个字符')
# 更复杂的密码强度检查
if not (any(c.isupper() for c in password) and
any(c.islower() for c in password) and
any(c.isdigit() for c in password)):
raise ValueError('密码必须包含大小写字母和数字')
return values
@root_validator
def validate_business_rules(cls, values):
# 复杂的业务规则验证
subscription_plan = values.get('subscription_plan')
custom_attrs = values.get('custom_attributes', {})
# 示例:企业版用户必须提供公司信息
if subscription_plan == 'enterprise':
if 'company_name' not in custom_attrs:
raise ValueError('企业版用户必须提供公司名称')
return values
1.2 动态验证与配置化验证规则
在实际应用中,验证规则可能需要动态变化。我们可以创建一个可配置的验证系统:
python
from pydantic import BaseModel, create_model
from typing import Any, Type
import json
class ValidationRule(BaseModel):
field_name: str
rule_type: str # 'required', 'regex', 'range', 'custom'
rule_value: Any
error_message: str
class DynamicValidator:
"""动态验证器生成器"""
@staticmethod
def create_model_from_rules(
model_name: str,
validation_rules: List[ValidationRule],
base_model: Type[BaseModel] = BaseModel
) -> Type[BaseModel]:
"""
根据验证规则动态创建Pydantic模型
"""
field_definitions = {}
validators = {}
# 构建字段定义
for rule in validation_rules:
field_type = str # 默认为字符串类型,可根据需要扩展
if rule.rule_type == 'range':
field_type = int
field_definitions[rule.field_name] = (
field_type, # 字段类型
... if rule.rule_type == 'required' else None # 是否必需
)
# 动态创建验证器函数
for rule in validation_rules:
def create_validator_func(rule_copy):
def validator_func(cls, value):
if rule_copy.rule_type == 'regex':
if not re.match(rule_copy.rule_value, str(value)):
raise ValueError(rule_copy.error_message)
elif rule_copy.rule_type == 'range':
min_val, max_val = rule_copy.rule_value
if not (min_val <= value <= max_val):
raise ValueError(rule_copy.error_message)
# 可以添加更多规则类型
return value
return validator_func
# 为每个字段创建验证器
validators[f'validate_{rule.field_name}'] = classmethod(
create_validator_func(rule)
)
# 动态创建模型类
return create_model(
model_name,
__base__=base_model,
**field_definitions,
__validators__=validators
)
# 使用示例
validation_rules = [
ValidationRule(
field_name="username",
rule_type="regex",
rule_value=r'^[a-zA-Z0-9_]{3,20}$',
error_message="用户名必须是3-20位的字母数字或下划线"
),
ValidationRule(
field_name="age",
rule_type="range",
rule_value=(18, 100),
error_message="年龄必须在18-100岁之间"
)
]
# 动态创建验证模型
UserModel = DynamicValidator.create_model_from_rules(
"DynamicUserModel",
validation_rules
)
# 现在可以使用这个动态创建的模型进行验证
try:
user = UserModel(username="john_doe123", age=25)
print("验证通过:", user)
except Exception as e:
print("验证失败:", e)
二、依赖注入与请求验证的深度融合
2.1 基于依赖注入的复杂验证
FastAPI 的依赖注入系统可以与验证逻辑深度整合,创建可重用、可测试的验证组件:
python
from fastapi import FastAPI, Depends, HTTPException, Query
from typing import Optional, List
from enum import Enum
import hashlib
app = FastAPI()
class ValidationDependencies:
"""验证相关的依赖注入类"""
@staticmethod
async def validate_api_key(
api_key: str = Query(..., description="API密钥")
) -> str:
"""
验证API密钥的有效性
"""
# 在实际应用中,这里应该查询数据库或缓存
valid_keys = {
"hash_of_real_key_1": "client_1",
"hash_of_real_key_2": "client_2"
}
hashed_key = hashlib.sha256(api_key.encode()).hexdigest()
if hashed_key not in valid_keys:
raise HTTPException(
status_code=401,
detail="无效的API密钥"
)
return valid_keys[hashed_key]
@staticmethod
async def validate_rate_limit(
client_id: str = Depends(validate_api_key),
redis_client = Depends(get_redis) # 假设有获取Redis的依赖
) -> bool:
"""
验证请求频率限制
"""
rate_limit_key = f"rate_limit:{client_id}"
current_count = await redis_client.incr(rate_limit_key)
if current_count == 1:
# 第一次请求,设置过期时间
await redis_client.expire(rate_limit_key, 60)
if current_count > 100: # 限制每分钟100次请求
raise HTTPException(
status_code=429,
detail="请求过于频繁,请稍后再试"
)
return True
@staticmethod
async def validate_content_type(
content_type: str = Header(default="application/json")
) -> str:
"""
验证内容类型
"""
allowed_types = ["application/json", "application/xml"]
if content_type not in allowed_types:
raise HTTPException(
status_code=415,
detail=f"不支持的内容类型。支持的类型: {', '.join(allowed_types)}"
)
return content_type
class OrderStatus(str, Enum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
CANCELLED = "cancelled"
class OrderUpdate(BaseModel):
status: OrderStatus
notes: Optional[str] = None
@validator('status')
def validate_status_transition(cls, v, values, **kwargs):
# 在实际应用中,这里会检查当前状态和允许的状态转换
# 示例:不允许从COMPLETED转换到其他状态
current_status = kwargs.get('current_status')
if current_status == OrderStatus.COMPLETED:
raise ValueError("已完成订单的状态不能更改")
return v
@app.put("/orders/{order_id}")
async def update_order(
order_id: str,
update_data: OrderUpdate,
# 使用多个验证依赖
client_id: str = Depends(ValidationDependencies.validate_api_key),
rate_limit_ok: bool = Depends(ValidationDependencies.validate_rate_limit),
content_type: str = Depends(ValidationDependencies.validate_content_type),
# 获取当前订单状态(模拟)
current_status: OrderStatus = Depends(get_order_status)
):
"""
更新订单状态
演示了多个验证依赖的使用
"""
# 注入当前状态到验证器
update_data.__pydantic_validator__.context = {
'current_status': current_status
}
# 执行更新逻辑
return {
"order_id": order_id,
"updated_data": update_data.dict(),
"client_id": client_id,
"message": "订单更新成功"
}
async def get_order_status(order_id: str) -> OrderStatus:
"""模拟获取订单状态的函数"""
# 在实际应用中,这里会查询数据库
return OrderStatus.PENDING
async def get_redis():
"""模拟获取Redis连接的函数"""
# 在实际应用中,这里会返回Redis连接
return None
2.2 验证中间件与全局验证
对于需要在多个端点应用的验证逻辑,我们可以使用中间件:
python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import time
import json
app = FastAPI()
class ValidationMiddleware:
"""自定义验证中间件"""
def __init__(self, app):
self.app = app
async def __call__(self, request: Request, call_next):
# 1. 请求前验证
validation_errors = await self.validate_request(request)
if validation_errors:
return JSONResponse(
status_code=400,
content={
"errors": validation_errors,
"message": "请求验证失败"
}
)
# 2. 添加请求时间戳
request.state.request_timestamp = time.time()
# 3. 验证请求体大小
content_length = request.headers.get("content-length")
if content_length and int(content_length) > 10 * 1024 * 1024: # 10MB限制
return JSONResponse(
status_code=413,
content={
"message": "请求体过大,最大允许10MB"
}
)
# 4. 调用下一个中间件或端点
response = await call_next(request)
# 5. 响应后处理
# 可以在这里添加响应验证逻辑
return response
async def validate_request(self, request: Request) -> List[dict]:
"""验证请求的各个部分"""
errors = []
# 验证请求方法
if request.method not in ["GET", "POST", "PUT", "DELETE", "PATCH"]:
errors.append({
"field": "method",
"error": f"不支持的HTTP方法: {request.method}"
})
# 验证必要的头部
required_headers = ["user-agent"]
for header in required_headers:
if header not in request.headers:
errors.append({
"field": f"header:{header}",
"error": f"缺少必要的头部: {header}"
})
# 验证路径参数(如果可能)
if "admin" in request.url.path and not request.headers.get("x-admin-token"):
errors.append({
"field": "header:x-admin-token",
"error": "访问管理员接口需要管理员令牌"
})
return errors
# 应用中间件
app.middleware("http")(ValidationMiddleware(app))
@app.post("/api/data")
async def receive_data(request: Request):
"""接收数据的端点,演示中间件验证"""
# 请求已经通过中间件验证
request_time = request.state.request_timestamp
try:
data = await request.json()
return {
"status": "success",
"request_time": request_time,
"data_received": data
}
except json.JSONDecodeError:
return JSONResponse(
status_code=400,
content={"error": "无效的JSON数据"}
)
三、数据库集成验证与原子性保证
3.1 数据库级别的唯一性验证
在分布式系统中,仅靠应用层验证是不够的,我们需要数据库级别的验证来保证数据一致性:
python
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy import create_engine, Column, String, Integer, DateTime, UniqueConstraint
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.exc import IntegrityError
from pydantic import BaseModel, validator
from datetime import datetime
from contextlib import contextmanager
import asyncpg # 对于异步PostgreSQL
app = FastAPI()
# SQLAlchemy 配置
SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/dbname"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class UserDB(Base):
"""数据库用户模型"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False, index=True)
email = Column(String(100), unique=True, nullable=False, index=True)
phone = Column(String(20), unique=True, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
# 复合唯一约束
__table_args__ = (
UniqueConstraint('username', 'email', name='uix_username_email'),
)
class UserCreate(BaseModel):