表单页面上邮箱格式校验通过,并不意味着发到服务端的数据就干净——攻击者用 curl 或 Postman 直接调接口,前端校验形同虚设。前后端校验看似重复,实际各守一道完全不同的边界:前端管体验,后端管安全。

客户端校验:体验优化器,不是防线

浏览器里的校验价值在于即时反馈:必填项没填、手机号位数不对,用户敲完立刻知道,不用等一个网络来回。它还能拦掉大部分无心之失,减少发到后端的无效请求。

但它的前提是”用户按你设计的路径操作”。请求一旦发出浏览器,就不受你控制了:改 JS、绕过页面、直接构造 HTTP 报文,都是几秒钟的事。所以客户端校验只能定位为 UX 优化,任何一条”只有前端校验”的规则,都等于没有规则。

服务端校验:分三层设防

服务端要对所有入参保持”默认不可信”的态度,校验按层次展开,各层失败语义不同:

第一层:参数校验

检查格式与结构——字段是否存在、类型对不对、长度是否超限、枚举值是否在白名单内、数值范围是否合理。这一层不查数据库,开销极小,在请求入口就完成,失败直接返回 400。

第二层:业务校验

检查数据在当前业务语境下是否成立——优惠券是否过期、订单当前状态允不允许退款、库存够不够。这类规则往往要读库,且涉及并发,应在事务内”先锁定再判断”,防止”查的时候有库存、扣的时候没了”。

第三层:持久层约束

数据库的唯一索引、非空约束、外键是最后一道兜底。典型误区是”先 SELECT 判断用户名不存在再 INSERT”来保证唯一——并发下两个请求会同时通过检查。唯一性必须靠数据库唯一约束保证,代码捕获冲突异常转成友好提示。

Bean Validation:注解式参数校验

Java 生态的 Bean Validation 规范(Hibernate Validator 为参考实现)允许在 DTO 字段上声明式标注约束,Controller 里加 @Valid 触发:

public class RegisterRequest {

@NotBlank(message = "用户名不能为空") // 非 null 且去空格后非空
@Size(min = 4, max = 20, message = "用户名长度需在 4-20 之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只允许字母数字下划线")
private String username;

@NotBlank
@Email(message = "邮箱格式不正确")
private String email;

@NotNull // 非空,但允许空字符串,注意与 @NotBlank 区别
@Min(value = 18, message = "必须年满 18 岁")
@Max(150)
private Integer age;
}
@PostMapping("/register")
public void register(@RequestBody @Valid RegisterRequest request) { ... }

几个容易踩的点:

  • @NotNull、@NotEmpty、@NotBlank 语义递进:@NotBlank 最严(去空白后非空),字符串字段基本都用它。
  • 校验 @PathVariable、@RequestParam 这类裸参数时,必须在类上加 @Validated,否则注解不生效。
  • Spring Boot 2.3 之后需显式引入 spring-boot-starter-validation 依赖。
  • 校验失败抛 MethodArgumentNotValidException,用全局异常处理器统一转成带字段路径的错误响应,别把内部堆栈回给调用方。

校验与 SQL 注入:分工而非替代

常有人问”做了输入校验,是不是就不怕 SQL 注入了?”答案是否定的,两者解决的问题不同:

  • **参数化查询(PreparedStatement)**是防 SQL 注入的根本手段。它把 SQL 结构和数据分开编译,用户输入永远只是”值”,不会改变语句结构。ORM(MyBatis 用 #{} 而非 ${})默认就是参数化的。
  • 输入校验是纵深防御的一层:白名单校验能提前挡掉明显恶意的载荷,减少攻击面,也能保证入库数据干净。

关键在不能反过来依赖:靠正则过滤单引号、OR、UNION 来防注入是黑名单思路,绕过手法层出不穷(编码变形、大小写、注释符)。正确姿势是强制参数化查询为主,白名单校验为辅。凡是无法参数化的场景(如动态排序字段名),必须用硬编码白名单映射,绝不拼接用户输入。

实践要点

  • 白名单优于黑名单:声明”允许什么”而不是罗列”禁止什么”。
  • 校验前先归一化(Unicode、大小写、全半角),防止用编码变形绕过规则。
  • 错误提示对攻击者说得太细等于送信息:对外返回稳定错误码即可,细节记日志。
  • 权限归属要在服务端查证:拿”当前登录用户 + 资源 ID”查库确认归属,不能信客户端传来的 userId。
  • 把恶意输入、越权请求、并发重复提交写进自动化测试,否则校验规则会随着重构悄悄失效。

小结

  • 客户端校验改善体验、减少无效请求,但可被轻易绕过,不构成安全边界。
  • 服务端分层设防:参数校验管格式,业务校验管规则,数据库约束兜底唯一性。
  • Bean Validation 用注解覆盖大部分格式校验,注意 @Validated 与三个 Not 注解的语义差异。
  • SQL 注入靠参数化查询根治,输入校验是纵深防御,二者分工不同、缺一不可。