Chapter 02 · 数据校验:让数据安全进门
📚 学完本章,你将能够:
- 解释"为什么后端不能信任前端传来的任何数据"
- 用 Pydantic 模型定义数据的形状和规则
- 设置字段约束:必填/可选、长度限制、数值范围、正则匹配
- 处理嵌套数据(如"订单里包含多个商品")
- 区分请求模型和响应模型,防止泄露敏感字段
💡 场景引入
你的图书 API 上线了。一个用户尝试注册,输入的用户名是 ""(空字符串),邮箱是 "not-an-email",密码只有 2 位。
如果没有校验,这些垃圾数据会直接存进你的"数据库"(目前还是内存列表),导致系统崩溃或安全漏洞。后端永远不应该相信前端传来的数据。
2.1 没有校验的世界有多可怕
看看不做校验的代码,想想会出现什么问题:
@app.post("/users")
async def create_user(data: dict): # 只是个字典,里面有什么完全不知道
users.append(data)
return data
用户可以这样攻击你:
// 请求 1:字段缺失
{ "username": "alice" } // 没有密码,没有邮箱
// 请求 2:类型错误
{ "username": 12345, "age": "hello" } // 用户名是数字,年龄是字符串
// 请求 3:恶意数据
{ "username": "", "password": "" }
你的代码必须手动处理每一种情况——这会让函数变成一团浆糊。Pydantic 就是来解决这个问题的。
2.2 Pydantic:数据的守门员
Pydantic 是 Python 的数据校验库,FastAPI 把它集成到了骨子里。核心概念很简单:你定义一个模型类,说明每个字段应该是什么类型、有什么限制,Pydantic 会自动检查传入的数据。
from pydantic import BaseModel
class UserCreate(BaseModel):
username: str
email: str
password: str
age: int | None = None # 可选字段,默认 None
当用户发送 POST 请求时,FastAPI 会:
接收原始 JSON
{"username": "alice", "email": "a@b.com"}
检查字段是否齐全
password 缺失 → 直接返回 422 错误
检查类型是否正确
age: "twenty" → 无法转成 int → 返回 422
构造合法对象
全部通过后,传入你的函数的是一个干净的 UserCreate 对象
2.3 给字段加规则:Field()
类型检查只是第一步。生产环境需要更精细的规则:用户名至少 3 个字符,密码至少 8 位,邮箱必须符合格式……
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
username: str = Field(min_length=3, max_length=20, description="用户登录名")
email: str = Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")
password: str = Field(min_length=8)
age: int | None = Field(default=None, ge=0, le=150)
| 约束 | 含义 | 示例 |
|---|---|---|
min_length / max_length | 字符串长度限制 | Field(min_length=3) |
ge / le | 大于等于 / 小于等于 | Field(ge=0, le=150) |
gt / lt | 大于 / 小于 | Field(gt=0)(价格必须为正) |
pattern | 正则表达式匹配 | Field(pattern=r"^\d{11}$")(手机号) |
default | 默认值 | Field(default="guest") |
在 Pydantic v2 中,Field() 的 default=... 和直接写 age: int | None = None 效果一样。后者更简洁,推荐用于简单的默认值。
2.4 响应模型:防止数据泄露
假设你的用户模型里有 password 字段。如果注册接口直接返回这个模型,密码就暴露给了前端!
class User(BaseModel):
id: int
username: str
email: str
password: str # 会暴露!
@app.post("/users", response_model=User)
async def create_user(data: UserCreate):
...
return new_user # 响应里包含 password 字段
class UserCreate(BaseModel): # 接收时用
username: str
email: str
password: str
class UserOut(BaseModel): # 返回时用
id: int
username: str
email: str
# 注意:没有 password!
@app.post("/users", response_model=UserOut)
async def create_user(data: UserCreate):
...
return new_user # password 被自动过滤
response_model=UserOut 的魔力在于:它会自动过滤掉响应对象中不在 UserOut 里的字段。即使你不小心返回了密码,它也不会出现在 JSON 响应中。
2.5 嵌套模型:处理复杂数据结构
现实中的数据往往是嵌套的。比如一个"订单"包含多个"商品项":
class OrderItem(BaseModel):
product_name: str
quantity: int = Field(gt=0)
unit_price: float = Field(gt=0)
class OrderCreate(BaseModel):
customer_name: str
items: list[OrderItem] # 嵌套列表!
address: str
@app.post("/orders")
async def create_order(order: OrderCreate):
total = sum(item.quantity * item.unit_price for item in order.items)
return {"total": total, "item_count": len(order.items)}
对应的 JSON 请求体:
{
"customer_name": "张三",
"address": "北京市海淀区",
"items": [
{ "product_name": "iPhone", "quantity": 1, "unit_price": 5999.0 },
{ "product_name": "AirPods", "quantity": 2, "unit_price": 999.0 }
]
}
Pydantic v2 中,把模型对象转成字典用 .model_dump(),转成 JSON 字符串用 .model_dump_json()。
user = UserOut(id=1, username="alice", email="a@b.com")
# 转字典
d = user.model_dump() # {"id": 1, "username": "alice", "email": "a@b.com"}
# 转 JSON 字符串
json_str = user.model_dump_json() # '{"id":1,...}'
# 排除未设置的字段(用于 PATCH 部分更新)
partial = user.model_dump(exclude_unset=True)
如果你在网上看到 .dict() 或 .json(),那是 Pydantic v1 的写法,v2 已经改名了。
2.6 枚举类型:限制可选值
如果某个字段只能取固定的几个值(如订单状态:待付款、已付款、已发货),用 Enum 比用字符串更安全:
from enum import Enum
class OrderStatus(str, Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
COMPLETED = "completed"
class OrderUpdate(BaseModel):
status: OrderStatus # 只能传入这四个值之一
🎯 知识检测
response_model 的作用是什么?response_model 有两个核心作用:1) 告诉客户端会收到什么结构;2) 自动过滤敏感字段。校验请求体是请求模型的职责。
| None 和默认值 = None。Field(ge=0, le=150) 设置范围。选项 A 没有默认值所以是必填;选项 C 的语法在 Python 3.10+ 可以但 Field 缺少 default;选项 D 的类型和默认值不匹配。
本章小结
后端不信任前端
任何从外部进来的数据都必须经过校验。这是安全的第一道防线。
Pydantic 模型 = 数据的合同
定义清楚字段、类型、限制,FastAPI 自动执行校验。
请求模型和响应模型要分开
防止密码等敏感字段随响应泄露。response_model 是安全网。
嵌套模型处理复杂 JSON
列表、嵌套对象用 Pydantic 依然优雅,不用手写解析代码。