请求验证器与 DTO 设计
本章内容:
- 理解请求验证在前后端交互中的防御性编程价值;
- 掌握基于
validator宏的 DTO 数据传输对象设计; - 熟练运用 length, email, range, regex 等常用验证规则;
- 学会处理复杂嵌套对象与数组列表的级联校验。
1. 验证器设计理念与防御性编程
Section titled “1. 验证器设计理念与防御性编程”Foundry 将数据验证直接与 Rust 的强类型系统深度绑定:
- 编译期强类型:通过
#[derive(Deserialize, Validate)]在 DTO 结构体字段上直接声明验证规则; - 非侵入式拦截:在控制器 Handler 顶部只需调用一行
payload.validate()?,若验证失败,框架会自动截断后续执行并返回结构化的 HTTP 422 校验失败响应; - 清晰的数据边界:DTO 隔离了外部网络请求结构与内部数据库实体模型,防止客户端恶意注入额外字段(Mass Assignment Vulnerability)。
2. 编写标准验证 DTO
Section titled “2. 编写标准验证 DTO”我们在子系统的 dto/ 目录(例如 src/systems/shop/dto/mod.rs)下统一定义请求和响应模型:
use serde::{Deserialize, Serialize};use validator::Validate;
/// 创建订单子项#[derive(Debug, Clone, Deserialize, Validate)]pub struct CreateOrderItem { #[validate(range(min = 1, message = "商品 ID 必须为合法正整数"))] pub product_id: i64,
#[validate(range(min = 1, max = 99, message = "单项购买数量必须在 1 到 99 之间"))] pub quantity: i32,}
/// 创建订单请求 DTO#[derive(Debug, Clone, Deserialize, Validate)]pub struct CreateOrderRequest { #[validate(length(min = 2, max = 32, message = "顾客姓名长度须为 2-32 字符"))] pub customer_name: String,
#[validate(email(message = "必须提供合法的电子邮箱地址"))] pub customer_email: String,
#[validate(length(min = 5, max = 255, message = "收货地址长度须在 5-255 字符之间"))] pub shipping_address: String,
// 嵌套结构列表校验:必须使用 nested 标注级联校验子项 #[validate(length(min = 1, message = "订单必须至少包含一件商品"))] #[validate(nested)] pub items: Vec<CreateOrderItem>,}
/// 分页查询请求 DTO (只读查询通常使用 Option 可选字段)#[derive(Debug, Clone, Deserialize)]pub struct OrderListQuery { pub status: Option<String>, pub page: Option<u64>, pub page_size: Option<u64>,}
/// 订单统一返回 DTO#[derive(Debug, Clone, Serialize)]pub struct OrderResponse { pub id: i64, pub order_no: String, pub customer_name: String, pub customer_email: String, pub total_amount: f64, pub status: String, pub created_at: chrono::DateTime<chrono::Utc>,}3. 常用校验规则速查清单
Section titled “3. 常用校验规则速查清单”| 规则名称 | 语法示例 | 说明 |
|---|---|---|
| 字符串长度 | #[validate(length(min = 6, max = 32))] |
限制字符长度(支持中英文) |
| 数值范围 | #[validate(range(min = 0.01, max = 99999.0))] |
限制整数或浮点数取值范围 |
| 电子邮箱 | #[validate(email)] |
校验是否符合标准邮箱格式 |
| URL 链接 | #[validate(url)] |
校验是否为合法 HTTP/HTTPS 链接 |
| 正则表达式 | #[validate(regex(path = *PHONE_REGEX))] |
正则模式匹配(如手机号、身份证) |
| 必须包含 | #[validate(contains = "@company.com")] |
检查字符串子串是否存在 |
| 集合大小 | #[validate(length(min = 1, max = 10))] |
限制 Vec<T> 数组元素个数 |
| 嵌套级联 | #[validate(nested)] |
递归校验子结构体中的所有验证规则 |
4. 控制器中一键校验
Section titled “4. 控制器中一键校验”在任何控制器中,验证入参极为清爽:
pub async fn create_order( Extension(ctx): Extension<SystemContext>, Extension(state): Extension<AppState>, Json(payload): Json<CreateOrderRequest>,) -> AppResult<ApiResponse<OrderResponse>> { // 1. 一行执行强校验:若非法自动截断并返回 HTTP 422 payload.validate()?;
// 2. 此时 payload 内部的所有数据均已保证符合业务约束 let order = OrderService::create_order(&ctx, &state, payload).await?; Ok(ApiResponse::created(order))}当客户端传入非法数据(如邮箱缺失 @ 或列表为空)时,客户端将立即收到标准化的错误返回:
{ "code": 422, "message": "Validation error: customer_email: 必须提供合法的电子邮箱地址", "data": null, "meta": null}5. 初学者避坑指南 (Beginner Gotchas)
Section titled “5. 初学者避坑指南 (Beginner Gotchas)”[!WARNING] 1. 嵌套数组必须加上
#[validate(nested)]: 如果你的请求体中包含对象数组(如Vec<CreateOrderItem>),仅在字段上声明length(min = 1)只能检查数组长度,不会自动触发每个子项的校验。必须额外添加#[validate(nested)]才会对数组中的每一项递归执行验证!2. 区分入参 DTO 与出参 DTO: 不要把出参(如包含敏感密码哈希的内部模型)直接暴露给前端。坚持使用独立的
ResponseDTO 并只标记Serialize,确保数据安全。