输入校验规范
前后端双重校验
[Requirement] 所有外部输入必须在前端和后端同时校验,前端校验用于提升体验(即时反馈),后端校验才是安全防线,不可省略。
[Requirement] 不得假设前端已校验,后端必须独立完成所有合法性、格式、范围校验,前端校验可被绕过。
| 层级 | 目的 | 校验内容 | 可绕过性 |
|---|---|---|---|
| 前端 | 体验优化、减少无效请求 | 格式、必填、长度、范围 | 可绕过(用户可改 JS / 用 curl) |
| 网关 | 第一层防御 | 方法、路径、大小、限流 | 不可绕过(必经) |
| 后端 | 安全底线 | 格式、范围、业务规则、权限 | 不可绕过 |
[Standard] 后端校验顺序:格式校验 → 类型校验 → 范围校验 → 业务规则校验 → 权限校验,越早失败越早返回,避免无效的下游调用。
[Caution] 禁止在业务逻辑中分散做参数校验,应在 Controller / DTO 入口集中完成。
[Standard] 必填字段在前端做非空提示,格式类字段(手机号、邮箱、身份证)用正则即时校验并给红框提示。
// 表单校验规则(示例:antd / element 通用)
const rules = {
phone: [
{ required: true, message: "请输入手机号" },
{ pattern: /^1[3-9]\d{9}$/, message: "手机号格式不正确" },
],
email: [
{ required: true, message: "请输入邮箱" },
{ type: "email", message: "邮箱格式不正确" },
],
age: [
{ required: true, message: "请输入年龄" },
{ type: "number", min: 0, max: 150, message: "年龄范围 0-150" },
],
};[Standard] 字符串字段限制最大长度(一般 ≤ 255,长文本 ≤ 10000),数值限制范围,前端通过 maxLength 与 max 属性约束并在提交前拦截。
[Caution] 前端校验失败时应阻止提交并定位到第一个错误字段,不允许静默放行。
[Caution] 前端不做安全相关校验(如权限、金额合法性),这些必须由后端判断;前端只做体验类校验。
[Requirement] 后端使用 DTO / Schema 在请求入口集中声明校验规则,业务层不再重复做格式校验。
# FastAPI + Pydantic 示例
from pydantic import BaseModel, Field, field_validator
from typing import Optional
class UserCreateDTO(BaseModel):
name: str = Field(..., min_length=1, max_length=50, description="用户名")
phone: str = Field(..., pattern=r"^1[3-9]\d{9}$", description="手机号")
age: int = Field(..., ge=0, le=150, description="年龄")
email: Optional[str] = Field(None, pattern=r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
@field_validator("name")
@classmethod
def no_special_chars(cls, v: str) -> str:
if any(c in v for c in "<>\\'\";"):
raise ValueError("用户名不能包含特殊字符")
return v[Requirement] 所有数值字段必须限制上下界(ge / le),金额字段必须用 Decimal 且限定小数位,禁止用 float。
[Requirement] 列表字段必须限制长度(如 max_length=100),防止超大列表导致 OOM。
from decimal import Decimal
from pydantic import BaseModel, Field
class OrderCreateDTO(BaseModel):
amount: Decimal = Field(..., gt=0, max_digits=12, decimal_places=2)
items: list[int] = Field(..., min_length=1, max_length=100)[Standard] 涉及业务规则的校验(如库存是否充足、是否重复下单)放在 Service 层,通过抛业务异常返回。
[Caution] 禁止把业务规则校验写成 if ... return {"error": ...} 的散落判断,应统一抛 BusinessError 由全局异常处理器处理。
class OrderService:
def create_order(self, dto: OrderCreateDTO, user_id: int):
product = self.product_repo.get(dto.product_id)
if product.stock < dto.quantity:
raise BusinessError("STOCK_INSUFFICIENT", "库存不足")
if self.order_repo.exists_unpaid(user_id, dto.product_id):
raise BusinessError("DUPLICATE_ORDER", "存在未支付订单,请先完成支付")[Requirement] 所有字符串输入必须做:去除首尾空白、规范化 Unicode(NFKC)、过滤控制字符。
import unicodedata
def sanitize(s: str) -> str:
s = s.strip()
s = unicodedata.normalize("NFKC", s)
# 去除控制字符(保留 \n \r \t)
s = "".join(c for c in s if c in "\n\r\t" or not unicodedata.category(c).startswith("C"))
return s| 语言 / 框架 | 校验方案 |
|---|---|
| Python / FastAPI | Pydantic v2(DTO 声明式) |
| Java / Spring Boot | @Valid + Bean Validation(Hibernate Validator) |
| Go | go-playground/validator |
| TypeScript / NestJS | class-validator |
| Node.js / Express | zod 或 joi |
[Standard] 优先使用框架内置的声明式校验,避免手写 if not x: raise ... 的重复样板。
[Requirement] 校验失败统一返回 422(或 400),错误响应体遵循统一格式,包含字段级错误信息。
{
"code": "VALIDATION_ERROR",
"message": "参数校验失败",
"request_id": "req-abc123",
"details": [
{ "field": "phone", "message": "手机号格式不正确" },
{ "field": "age", "message": "输入应小于等于 150" }
]
}[Standard] 对复用率高的校验规则(手机号、身份证、统一社会信用代码)封装为可复用的校验器注解 / 函数。
# 身份证校验(18 位,含校验位)
def validate_id_card(value: str) -> str:
if not re.match(r"^\d{17}[\dXx]$", value):
raise ValueError("身份证格式不正确")
weights = [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]
codes = "10X98765432"
check = codes[sum(int(value[i]) * weights[i] for i in range(17)) % 11]
if check != value[17].upper():
raise ValueError("身份证校验位错误")
return value| 反模式 | 说明 | 风险 |
|---|---|---|
| 只做前端校验 | 后端完全依赖前端传过来的值 | 攻击者绕过前端直接调用接口,可注入非法数据 |
| 在业务代码散落校验 | Service 中到处 if not x: return error | 校验不一致,遗漏字段,难以维护 |
用 eval / exec 处理输入 | 动态执行用户输入 | 远程代码执行(RCE) |
| 字符串拼接 SQL | f"SELECT * FROM users WHERE name='{name}'" | SQL 注入 |
| 不限制列表/字符串长度 | 接受任意长度输入 | OOM、慢查询、DoS |
| 金额用 float | amount: float | 浮点精度丢失,金额计算错误 |
| 不做输入清洗 | 直接存储原始输入 | XSS、控制字符绕过、Unicode 同形字攻击 |
| 信任客户端传的用户 ID | 用前端传的 user_id 查数据 | 水平越权(必须从 Token 取当前用户) |
| 校验失败返回 200 | 用 {"success": false} 表示失败 | 状态码语义错误,客户端难以统一处理 |