接口作为系统与外部交互的入口,不可避免会接收各种输入数据 —— 恶意的攻击参数、格式错误的值、超出业务范围的异常数据,都可能导致系统崩溃、数据污染或业务逻辑错误。数据校验体系通过 “多层过滤”“精准校验”“友好提示” 三重机制,在数据进入业务逻辑前进行净化,是保障系统稳定和数据质量的 “第一道防线”。
数据校验的核心价值与层级划分
为什么需要数据校验?
- 防止垃圾数据入库:避免格式错误(如手机号含字母)、逻辑矛盾(如订单金额为负)的数据污染数据库
- 减少业务异常:提前拦截无效输入,降低下游业务逻辑的异常处理成本
- 抵御恶意攻击:过滤 SQL 注入(如参数含
' or 1=1)、XSS 攻击(如含<script>标签)等恶意输入 - 提升用户体验:及时返回明确的错误提示(如 “手机号格式不正确”),避免用户无效操作
校验的三层防御体系
- 网关层校验:拦截明显非法的请求(如请求体过大、IP 黑名单)
- 接口层校验:验证参数格式、必填项、数据范围(如 “年龄必须大于 0”)
- 业务层校验:验证业务逻辑合法性(如 “库存不足,无法下单”)
接口层校验:参数合法性的直接保障
1. 基于注解的声明式校验
使用 JSR-303(Bean Validation)规范的注解,简洁高效地完成参数校验:
// 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
// 请求参数类
@Data
public class UserRegisterDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 4, max = 20, message = "用户名长度必须在4-20之间")
private String username;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$",
message = "密码至少8位,包含字母和数字")
private String password;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\d{9}$", message = "手机号格式不正确")
private String phone;
@NotNull(message = "年龄不能为空")
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能大于150")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email; // 非必填,但填了就必须符合格式
}
// 接口中启用校验
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping("/register")
public Result register(@Valid @RequestBody UserRegisterDTO dto,
BindingResult bindingResult) {
// 处理校验失败的情况
if (bindingResult.hasErrors()) {
// 获取第一个错误信息返回
String errorMsg = bindingResult.getFieldError().getDefaultMessage();
return Result.fail(400, errorMsg);
}
// 校验通过,执行注册逻辑
userService.register(dto);
return Result.success("注册成功");
}
}
常用校验注解:
- 空值校验:
@NotNull(不能为 null)、@NotBlank(字符串不能为 null 且 trim 后不为空)、@NotEmpty(集合不能为 null 且不为空) - 数值校验:
@Min/@Max(数值范围)、@DecimalMin/@DecimalMax(小数范围) - 长度校验:
@Size(集合 / 字符串长度)、@Length(字符串长度) - 格式校验:
@Pattern(正则匹配)、@Email(邮箱格式)
2. 全局异常处理:统一返回校验错误
通过@ControllerAdvice统一处理校验异常,避免在每个接口中重复编写错误处理逻辑:
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidationException(MethodArgumentNotValidException e) {
// 收集所有错误信息(也可只返回第一个)
List<String> errorMessages = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
return Result.fail(400, String.join(";", errorMessages));
}
// 处理路径参数/请求参数校验异常
@ExceptionHandler(ConstraintViolationException.class)
public Result handleConstraintViolationException(ConstraintViolationException e) {
String errorMsg = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.findFirst()
.orElse("参数校验失败");
return Result.fail(400, errorMsg);
}
}
// 接口中无需再处理BindingResult
@PostMapping("/register")
public Result register(@Valid @RequestBody UserRegisterDTO dto) {
// 直接执行业务逻辑(校验失败会被全局异常处理器捕获)
userService.register(dto);
return Result.success("注册成功");
}
业务层校验:复杂逻辑的深度验证
注解校验适合简单规则,复杂的业务逻辑校验(如 “用户余额是否足够支付订单”)需要在业务层实现:
@Service
public class OrderService {
@Autowired
private UserMapper userMapper;
@Autowired
private ProductMapper productMapper;
public Long createOrder(OrderCreateDTO dto) {
// 1. 业务规则校验:用户是否存在
User user = userMapper.selectById(dto.getUserId());
if (user == null) {
throw new BusinessException("用户不存在");
}
// 2. 业务规则校验:商品是否存在且库存充足
Product product = productMapper.selectById(dto.getProductId());
if (product == null) {
throw new BusinessException("商品不存在");
}
if (product.getStock() < dto.getQuantity()) {
throw new BusinessException("商品库存不足,当前库存:" + product.getStock());
}
// 3. 业务规则校验:金额是否合理(防止负数或过大值)
if (dto.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("订单金额必须大于0");
}
if (dto.getAmount().compareTo(new BigDecimal("1000000")) > 0) {
throw new BusinessException("订单金额不能超过100万");
}
// 4. 执行下单逻辑(省略)
return orderId;
}
}
// 自定义业务异常
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
// 全局异常处理器中添加业务异常处理
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
return Result.fail(400, e.getMessage());
}
特殊场景的校验处理
1. 防 SQL 注入与 XSS 攻击
对用户输入的文本进行净化,避免恶意脚本或 SQL 片段:
@Component
public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 使用包装类处理请求参数,过滤XSS脚本
chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response);
}
}
// XSS请求包装类
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
// 过滤HTML标签和特殊字符
private String cleanXss(String value) {
if (value == null) {
return null;
}
// 过滤<script>等标签
value = value.replaceAll("<script>", "<script>");
value = value.replaceAll("</script>", "</script>");
// 过滤SQL注入相关字符
value = value.replaceAll("'", "''");
return value;
}
// 重写获取参数的方法,对参数值进行净化
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return cleanXss(value);
}
// 重写获取请求体的方法(针对JSON参数)
@Override
public ServletInputStream getInputStream() throws IOException {
// 读取原始请求体
String body = IOUtils.toString(super.getInputStream(), StandardCharsets.UTF_8);
// 净化后写回
byte[] cleanBody = cleanXss(body).getBytes(StandardCharsets.UTF_8);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return 0;
}
@Override
public boolean isFinished() {
return false;
}
// 其他方法实现省略...
};
}
}
2. 批量数据校验
对批量提交的数据(如批量创建用户),需逐个校验并返回详细错误:
@PostMapping("/batch-register")
public Result batchRegister(@Valid @RequestBody List<UserRegisterDTO> userList) {
List<String> errorList = new ArrayList<>();
// 逐个校验
for (int i = 0; i < userList.size(); i++) {
UserRegisterDTO user = userList.get(i);
// 使用Validator手动校验
Set<ConstraintViolation<UserRegisterDTO>> violations = validator.validate(user);
if (!violations.isEmpty()) {
String errors = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";"));
errorList.add("第" + (i+1) + "条数据错误:" + errors);
}
}
if (!errorList.isEmpty()) {
return Result.fail(400, "批量注册失败", errorList);
}
// 校验通过,执行批量注册
userService.batchRegister(userList);
return Result.success("批量注册成功");
}
校验体系的最佳实践
1. 校验规则与业务文档同步
确保校验规则在接口文档中明确说明(如 Swagger 文档):
@Data
public class UserRegisterDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 4, max = 20, message = "用户名长度必须在4-20之间")
@Schema(description = "用户名", example = "zhangsan", required = true)
private String username;
// 其他字段...
}
2. 校验逻辑的可复用性
将通用校验逻辑封装为工具类或自定义注解:
// 自定义注解:验证身份证号
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdCardValidator.class)
public @interface IdCard {
String message() default "身份证号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 自定义校验器
public class IdCardValidator implements ConstraintValidator<IdCard, String> {
// 身份证号正则(简化版)
private static final String REGEX = "^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$";
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // 允许为null,非空校验由@NotBlank处理
}
return value.matches(REGEX);
}
}
// 使用自定义注解
public class UserDTO {
@IdCard(message = "身份证号格式错误")
private String idCard;
}
避坑指南
-
校验不要过度前移:复杂业务校验放在业务层而非接口层(避免接口参数类过于复杂)
-
错误提示要具体:避免 “参数错误” 这种模糊提示,应明确指出 “手机号格式不正确”
-
区分前端校验和后端校验:前端校验仅提升体验,后端校验才是安全保障(防止绕过前端直接调用接口)
-
性能平衡:批量数据校验时注意性能(如 1000 条数据逐个校验,避免 O (n²) 复杂度)
数据校验体系的核心是 “在合适的层级做合适的校验”—— 网关层挡掉明显攻击,接口层保证参数合规,业务层确保逻辑正确。一个完善的校验体系,能让系统在面对各种输入时保持稳健,这是后端接口 “可靠性” 的基础保障。