后端数据校验:从源头拦截无效请求

110 阅读4分钟

在后端开发中,无效请求是系统故障的重要源头 —— 格式错误的手机号、超出范围的数量、缺失的必填参数,这些问题如果放任到业务逻辑层,可能导致数据库异常、业务逻辑混乱甚至系统崩溃。数据校验机制通过在接口入口处拦截无效数据,既能减少下游处理压力,也能为前端提供清晰的错误提示。

数据校验的核心原则

数据校验的核心是 “入口拦截,尽早失败”,需遵循:

  • 全面性:覆盖所有外部输入(URL 参数、请求体、请求头)
  • 精确性:错误提示需明确到具体字段(如 “手机号格式错误” 而非 “参数错误”)
  • 层次性:前端做基础校验(如非空),后端做完整校验(如格式、业务规则)
  • 一致性:前后端校验规则保持一致(如密码长度要求)

主流校验实现方案

1. 注解式校验:Spring Validation 的便捷应用

利用 JSR-303 规范的注解(如@NotNull@Pattern),配合 Spring Validation 实现声明式校验。

代码示例

// 1. 定义DTO并添加校验注解
@Data
public class UserRegisterDTO {
    @NotNull(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
    private String username;

    @NotNull(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\d{9}$", message = "手机号格式错误")
    private String phone;

    @NotNull(message = "密码不能为空")
    @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z]).{8,16}$", message = "密码必须包含数字和字母,长度8-16位")
    private String password;

    @NotNull(message = "年龄不能为空")
    @Min(value = 0, message = "年龄不能小于0")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;
}

// 2. 接口中启用校验
@RestController
@RequestMapping("/users")
public class UserController {
    @PostMapping("/register")
    public Result register(@Valid @RequestBody UserRegisterDTO dto, BindingResult bindingResult) {
        // 3. 处理校验结果
        if (bindingResult.hasErrors()) {
            List<String> errorMsg = bindingResult.getFieldErrors().stream()
                    .map(FieldError::getDefaultMessage)
                    .collect(Collectors.toList());
            return Result.fail("参数校验失败", errorMsg);
        }
        // 校验通过,执行业务逻辑
        userService.register(dto);
        return Result.success("注册成功");
    }
}

常用校验注解

  • 非空校验:@NotNull(对象)、@NotEmpty(字符串 / 集合)、@NotBlank(字符串去空格后非空)
  • 数值校验:@Min@Max@DecimalMin@DecimalMax
  • 格式校验:@Pattern(正则表达式)、@Email(邮箱格式)
  • 长度校验:@Size(集合 / 字符串长度)、@Length(字符串长度)

2. 自定义校验:应对复杂业务规则

对于注解无法覆盖的业务规则(如 “邀请码必须存在且未被使用”),可自定义校验注解和校验器。

代码示例

// 1. 自定义校验注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = InviteCodeValidator.class) // 指定校验器
public @interface ValidInviteCode {
    String message() default "邀请码无效";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 2. 实现校验器
public class InviteCodeValidator implements ConstraintValidator<ValidInviteCode, String> {
    @Autowired
    private InviteCodeService inviteCodeService; // 注入业务服务

    @Override
    public boolean isValid(String code, ConstraintValidatorContext context) {
        if (code == null) {
            return false; // 邀请码不能为空(可结合@NotNull使用)
        }
        // 业务校验:邀请码存在且未被使用
        return inviteCodeService.existsAndUnused(code);
    }
}

// 3. 在DTO中使用
@Data
public class UserRegisterDTO {
    // ... 其他字段
    @ValidInviteCode(message = "邀请码不存在或已被使用")
    private String inviteCode;
}

校验的最佳实践

1. 分层校验策略

  • Controller 层:校验参数格式、必填项、基本范围(如手机号格式)
  • Service 层:校验业务规则(如 “该手机号已注册”“库存不足”)
  • Repository 层:校验数据存储约束(如唯一索引冲突)

2. 错误信息标准化

返回的错误信息应包含:

  • 具体字段名(便于前端定位输入框)

  • 错误原因(如 “手机号已被注册” 而非 “操作失败”)

  • 解决方案(如 “请更换其他手机号”)

示例

{
  "code": 400,
  "message": "参数校验失败",
  "details": [
    { "field": "phone", "message": "手机号已被注册,请更换其他手机号" },
    { "field": "password", "message": "密码必须包含数字和字母,长度8-16位" }
  ]
}

3. 性能优化

  • 避免在校验器中执行耗时操作(如远程调用),必要时异步校验
  • 复杂校验可先做基础过滤(如邀请码长度不对直接返回),再做深层校验

避坑指南

  • 不要依赖前端校验:前端校验可被绕过,后端必须做完整校验

  • 避免过度校验:如对内部服务调用的参数,可适当简化校验(信任内部系统)

  • 校验逻辑与业务逻辑分离:避免在业务代码中混杂大量 if-else 校验语句

数据校验看似是 “基础工作”,实则是系统稳定性的第一道防线。一个设计良好的校验体系,能拦截 80% 以上的无效请求,让业务逻辑层更专注于核心流程,这正是 “善战者,无赫赫之功” 的后端开发智慧。