SpringBoot2 | 第二十六篇(一):数据验证

​ 对于任何一个应用而言,客户端做的数据有效性验证都不是安全有效的,而数据验证又是一个企业级项目架构上最为基础的功能模块,这时候就要求我们在服务端接收到数据的时候也对数据的有效性进行验证。为什么这么说呢?往往我们在编写程序的时候都会感觉后台的验证无关紧要,毕竟客户端已经做过验证了,后端没必要在浪费资源对数据进行验证了,但恰恰是这种思维最为容易被别人钻空子。毕竟只要有点开发经验的都知道,我们完全可以模拟 HTTP 请求到后台地址,模拟请求过程中发送一些涉及系统安全的数据到后台,后果可想而知…. 接下来下文会记录 Java后端 如何做数据验证…

​ —— 摘自唐亚峰前辈的描述

就一句话,前后端都得校验

[TOC]

JSR-303 注释介绍

JSR-303 是JAVA EE 6 中的一项子规范,叫做 Bean ValidationHibernate ValidatorBean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。Java 后台数据验证 就是基本它实现的。

​ 这里只列举了 javax.validation 包下的注解,同理在 spring-boot-starter-web 包中也存在 hibernate-validator 验证包,里面包含了一些 javax.validation 没有的注解,有兴趣的可以看看

注解 说明
@NotNull 限制 必须 不为null
@NotEmpty 验证注解的元素值 不为 null 且不为空(这里的空指:字符串长度不为0、集合size() 大小不为0)
@NotBlank 验证注解的元素值 不为空(不为null、去除首位空格 后长度不为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Pattern(value) 限制 必须符合 指定的正则表达式
@Size(max,min) 限制字符长度必须在 minmax 之间(也可以用在集合上)
@Email 验证注解的元素值是Email,也可以通过正则表达式和 flag 指定自定义的email格式
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Null 限制只能为null(很少用)
@AssertFalse 限制必须为false (很少用)
@AssertTrue 限制必须为true (很少用)
@Past 限制必须是一个 过去 的日期
@Future 限制必须是一个 将来 的日期
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过 integer,小数部分的位数不能超过 fraction (很少用)
@Size(max, min) 带注释的元素(Collection、Map等等)的长度必须在指定的范围内
@Range(max, min) 带注释的元素的大小必须在指定的范围内
@Positive 带注释的元素必须是一个严格的正数(即0被视为无效值)
@PositiveOrZero 带注释的元素必须为一个正数或者0

环境/版本一览:

  • 开发工具:Intellij IDEA 2018.2.2
  • springboot: 2.1.0.RELEASE
  • jdk:1.8.0_171
  • maven:3.3.9

1、pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

2、application.yml

1
2
server:
port: 8088

3、entity

Book.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.fatal.entity;

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.Valid;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
import java.util.List;

/**
* Book 实体
* @author: Fatal
* @date: 2018/11/27 0027 16:56
*/
@Data
public class Book {

private Long id;

@NotBlank(message = "name 不能为空")
@Length(min = 2, max = 10, message = "name 长度必须在 {min} - {max} 之间")
private String name;

@NotNull(message = "price 不允许为空")
@DecimalMin(value = "0.1", message = "价格不能低于 {value}")
private BigDecimal price;

@Valid // 嵌套验证必须用@Valid
@NotNull(message = "readers 不能为空")
@Size(min = 1, message = "readers 至少要有一个自定义属性")
private List<Reader> readers;

}

Reader.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.fatal.entity;

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotBlank;

/**
* @author: Fatal
* @date: 2018/11/27 0027 18:22
*/
@Data
public class Reader {

private Long id;

@NotBlank(message = "readerName 不能为空")
@Length(min = 2, max = 10, message = "readerName 长度必须在 {min} - {max} 之间")
private String readerName;

}

注解介绍

  • @Validated: 开启数据有效性校验,添加在类上即为验证方法,添加在方法参数中即为验证参数对象。(添加在方法上无效)
  • @NotBlank: 被注释的字符串不允许为空(value.trim() > 0 ? true : false
  • @Length: 被注释的字符串的大小必须在指定的范围内
  • @NotNull: 被注释的字段不允许为空(value != null ? true : false)
  • @DecimalMin: 被注释的字段必须大于或等于指定的数值

上面的条件运算符取自注解校验器的方法实现

4、controller

此处只是为了图方便只写在了 Controller 层,同理你可以将它作用在 Service 层)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.fatal.controller;

import com.fatal.entity.Book;
import org.hibernate.validator.constraints.Length;
import org.springframework.http.MediaType;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.NotBlank;

/**
* 数据验证控制层
* @author: Fatal
* @date: 2018/11/27 0027 17:10
*/
@Validated // 开启属性参数验证
@RestController
public class ValidatorController {

/**
* 测试属性参数验证
*/
@GetMapping("/test1")
public String test1(@NotBlank(message = "name 不能为空")
@Length(min = 2, max = 10, message = "name 长度必须在 {min} - {max} 之间")
String name) {
/**
* @Validated 标注在类上与 @Length... 这些注解一起校验方法参数时。
* 这时候`BindingResult`不能起作用,
* 错误信息返回JSON中取错误信息。
* {
* "timestamp": "2018-11-27T10:13:05.743+0000",
* "status": 500,
* "error": "Internal Server Error",
* "message": "test1.name: name 不能为空",
* "path": "/test1"
* }
*/
return "success";
}

/**
* 测试对象参数验证和嵌套验证
*/
@PostMapping(value = "/test2", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String test2(@Validated @RequestBody Book book, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return bindingResult.getFieldError().getDefaultMessage();
}
return "success";
}

}

显示

启动项目

可以使用 IDEA 自带的 RestClient 测试,也可以使用 Postman 测试

  1. 访问 http://localhost:8088/test1

    IDEA 自带的 Rest Client(name长度为1)

    1543372128357

    1543372142667

Postman(name长度为1)

1543372254573

  1. 访问 http://localhost:8088/test2

    • IDEA 自带的 Rest Client(readerName长度为1)

    1543371638533

    1543372330555

    • Postman(readerName长度为1)

    1543371852659

    1543372372175

笔记

1、@Valid 和 @Validated

javax提供了@Valid(标准JSR-303规范),Spring Validation 验证框架对参数的验证机制提供了@Validated(Spring’s JSR-303规范,是标准JSR-303的一个变种)。@Validated 或者 @Valid 在基本验证功能上没有太多区别,但是在分组、注解地方、嵌套验证等功能上两个有所不同:

  1. 分组
    @Validated:提供了一个 分组 功能,可以在入参验证时,根据不同的分组采用不同的验证机制,第三篇探究。
    @Valid:不提供分组功能。
  2. 注解地方
    @Validated:可以用在类型、方法和方法参数上,不能 用在成员属性(字段)上
    @Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
    注:两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。
  3. 嵌套验证
    嵌套的属性需要验证时,在该属性上标注开启嵌套验证的注解
    @Validated:不可以(因为它标注在成员属性上不起作用)
    @Valid:可以

2、嵌套验证格式总结

在方法上我们用 @Validated + BindingResult

1
2
3
4
5
6
7
@PostMapping(value = "/test2", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String test2(@Validated @RequestBody Book book, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return bindingResult.getFieldError().getDefaultMessage();
}
return "success";
}

在实体类中,对 entityList<entity> 等等的与 entity 类型相关的成员属性,需要在这些属性上添加 @Valid 注解

1
2
3
4
5
6
7
8
9
10
11
@Data
public class Book {

...

@Valid // 嵌套验证必须用@Valid,才会集合里边的每一个`Reader`的属性
@NotNull(message = "readers 不能为空")
@Size(min = 1, message = "readers 至少要有一个自定义属性")
private List<Reader> readers;

}
1
2
3
4
5
6
7
8
9
10
@Data
public class Reader {

private Long id;

@NotBlank(message = "readerName 不能为空")
@Length(min = 2, max = 10, message = "readerName 长度必须在 {min} - {max} 之间")
private String readerName;

}

3、@Validated校验参数的问题

3.1、获取异常信息

当 @Validated 放在类上,结合 JSR 注解 校验参数的时候,不能与 BindingResult 或者 Errors 一起使用,所有我们要是想取校验信息,可以采取别的方式。这里的方式是我在最近一个项目中用的,很简单,就是全局异常处理器获取异常信息并返回。

这里列举一个异常处理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.gzxant.exception;

import com.gzxant.enums.HttpCodeEnum;
import com.gzxant.utils.ResultVOUtils;
import com.gzxant.vo.ResultVO;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.internal.engine.ConstraintViolationImpl;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Set;

/**
* 自定义全局异常处理类
* @author: Fatal
* @date: 2018/12/12 0012 15:24
*/
@Slf4j
@ControllerAdvice
public class CupExceptionHandler {

/**
* 处理JSR参数校验异常(针对参数位置的校验注解产生的异常)
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public ResultVO global(ConstraintViolationException e) {
Set<ConstraintViolation<?>> set = e.getConstraintViolations();
ConstraintViolationImpl constraintViolation = (ConstraintViolationImpl)set.toArray()[0];
return ResultVOUtils.fail(constraintViolation.getMessage());
}

}

由方法实现中知,校验出现的异常信息封装在 Set<ConstraintViolation<?>> 中,我们只想取出即可。已测试能用

3.2、@RequestParam + @NotBlank 不起作用

​ 必须使用 @RequestParam(value="",defaultValue="") 来接受单参,注意,必须要有默认值空串,否则 @NotBlank 不起作用

1
2
3
4
5
6
7
@GetMapping
public String get(@NotBlank(message = "用户uid(uid) 不能为空")
@RequestParam(value = "uid", defaultValue = "")
String uid) {

return "success";
}

4、BindingResult 起不了作用

问题描述:

刚开始我这样写。在校验的实体和 BindingResult 之间隔了个 Model

1557199462460

然后 BindingResult 就不起作用。

解决:

校验的实体和 BindingResult 紧挨一起

1557199536929

BindingResult 就能正常使用了。

结论:

BindingResult必须紧跟在校验实体后面

参考资料

一起来学SpringBoot | 第十九篇:轻松搞定数据验证(一)

@Validated和@Valid区别:Spring validation验证框架对入参实体进行嵌套验证必须在相应属性(字段)加上@Valid而不是@Validated

总结

SpringBoot的知识已经有前辈在我们之前探索了。比较喜欢的博主有:唐亚峰 | Battcn方志朋的专栏程序猿DD纯洁的微笑。对这门技术感兴趣的可以去他们的博客逛逛。谢谢他们的分享~~

以上文章是我用来学习的Demo,都是基于 SpringBoot2.x 版本。

源码地址: https://github.com/ynfatal/springboot2-learning/tree/master/chapter26_1

学习 唐亚峰 前辈的经验