跳转到内容

输入校验规范

前后端双重校验


[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 / FastAPIPydantic v2(DTO 声明式)
Java / Spring Boot@Valid + Bean Validation(Hibernate Validator)
Gogo-playground/validator
TypeScript / NestJSclass-validator
Node.js / Expresszod 或 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)
字符串拼接 SQLf"SELECT * FROM users WHERE name='{name}'"SQL 注入
不限制列表/字符串长度接受任意长度输入OOM、慢查询、DoS
金额用 floatamount: float浮点精度丢失,金额计算错误
不做输入清洗直接存储原始输入XSS、控制字符绕过、Unicode 同形字攻击
信任客户端传的用户 ID用前端传的 user_id 查数据水平越权(必须从 Token 取当前用户)
校验失败返回 200用 {"success": false} 表示失败状态码语义错误,客户端难以统一处理