原文地址,转载请注明出处: https://blog.youkuaiyun.com/qq_34021712/article/details/87545287 ©王赛超
Spring Boot已经可以帮助我们快速构建一个web服务,针对请求的参数如果只在客户端进行数据有效性验证都不是安全有效的,我们可以进行抓包直接调用接口,所以我们需要在客户端做参数校验,同时后端服务也需要对参数进行校验才可以。所以经常会看到类似下面这样的代码:
你会发现整个方法才100
行,其中80
行都是参数校验,大量的参数校验代码充斥在业务代码中,看着很不爽,而且不少浪费时间,本篇将帮助你使用SpringBoot
实现RESTAPI
服务的有效验证。
什么是spring-boot-starter-validation?
我们都知道 Spring Boot
的 Starters
机制,只要导入相应的包,我们就可以快速的使用对应的api,所以我们只要加入 spring-boot-starter-validation
这个 Starter
,就可以使用其实现验证。那什么是 spring-boot-starter-validation
?
spring-boot-starter-validation
就是使用 Hibernate Validator
框架来提供 Java Bean
验证功能。
看到 Hibernate
开头,大家首先想到的就是 Hibernate ORM
框架,Hibernate Validator
是Hibernate
项目中的一个数据校验框架,是 JSR 380
参考实现。Hibernate Validator
、Bean Validation API
和 TCK
都是使用了Apache Software License 2.0
。
Hibernate Validator 6
和 Bean Validation 2.0
需要 Java8
或更新版本。
英文文档地址:
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single
Spring Boot中使用spring-boot-starter-validation
pom添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
参数校验demo
配置要验证的请求实体
public class ModelCreateUserRequest {
/** 姓名 */
@NotBlank(message = "用户姓名不能为空")
private String name;
/** 身份证号 */
@Pattern(regexp="^(\\d{18}|\\d{17}(\\d{1}|[X|x]))$",message="身份证格式不正确")
private String idCardNum;
/** 年龄 */
@NotNull(message="用户年龄不能为空")
@Min(value = 1,message = "年龄不正确")
@Max(value = 150,message = "年龄不正确")
private Integer age;
/** 家庭住址 */
@NotBlank(message = "用户地址不能为空")
private String address;
/** 是否已确认 */
@NotNull(message="用户必须确认")
@AssertTrue(message = "用户必须确认")
private Boolean confirmResult;
//省略 get set 方法
}
Controller方法配置
@RequestMapping(value = "/createUser",method = RequestMethod.POST)
@ResponseBody
public void createUser(@RequestBody @Valid ModelCreateUserRequest modelCreateUserRequest,BindingResult result){
if(result.hasErrors()){
for (ObjectError error : result.getAllErrors()) {
System.out.println(error.getDefaultMessage());
}
}
}
POST请求传入的参数:
{"name":"","idCardNum":"","age":"","address":"","confirmResult":""}
输出结果:
用户年龄不能为空
用户姓名不能为空
用户地址不能为空
身份证格式不正确
用户必须确认
使用方式非常简单,在需要绑定的bean
上直接添加注解,然后在 @RequestBody ModelCreateUserRequest modelCreateUserRequest之间加 @Valid
注解,将校验结果放到BindingResult
对象中。
如果要检验的参数类型不能使用某个注解校验的时候,就会报异常,例如 在Integer
类型上使用 Pattern
注解,就会报下面的异常,Integer
类型无法使用 Pattern
注解来校验:
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint ‘javax.validation.constraints.Pattern’ validating type ‘java.lang.Integer’. Check configuration for ‘age’。
Hibernate Validator的校验模式
上面的demo中,一次性打印了所有验证不符合规则的属性,通常按顺序验证到第一个字段不符合验证要求时,就可以直接拒绝请求了。Hibernate Validator有以下两种验证模式:
普通模式(默认模式)
普通模式会校验完所有的属性,然后返回所有的验证失败信息。这也是默认使用的模式。
快速失败
Hibernate Validator也支持在第一次发生约束违规时立即从当前验证返回。
两种配置方式
官网地址:
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-provider-specific-settings
第一种:设置Hibernate Validator特定选项
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
第二种:可以通过Configuration#addProperty()传递特定选项
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
Spring Boot 配置hibernate Validator为快速失败模式:
@Configuration
public class ValidatorConfiguration {
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast(true)
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
捕获全局参数校验异常并返回错误提示信息
上面我在绑定请求参数的bean
后面 添加了 BindindResult
参数,然后在方法体内处理不符合规则的属性,并响应给客户端,但是如果在每个方法体内,都写一遍处理逻辑又是很麻烦的操作,所以我们可以使用下面的操作。
首先将 参数bean
后面的 BindindResult
去掉,这个时候,再请求将会抛出 org.springframework.web.bind.MethodArgumentNotValidException
。
后面我们要讲的: 直接对方法内的单个添加了 @RequestParam
参数校验时,抛出的是 javax.validation.ConstraintViolationException
。
我们可以捕获这两个异常,在这里进行统一处理,并返回。例如:
@ControllerAdvice
@Component
public class GlobalExceptionHandlerAdvice {
@ExceptionHandler
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ReturnResult handleConstraintViolationException(ConstraintViolationException exception) {
Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
StringBuilder stringBuilder = new StringBuilder();
for (ConstraintViolation<?> item : violations) {
//将所有的异常信息封装成String
stringBuilder.append(item.getMessage()).append(" ");
}
ReturnResult returnResult = new ReturnResult();
returnResult.setCode(HttpStatus.BAD_REQUEST.value());
returnResult.setMessage(stringBuilder.toString());
return returnResult;
}
@ExceptionHandler
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ReturnResult handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
BindingResult bindingResult = exception.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
StringBuilder stringBuilder = new StringBuilder();
for (FieldError item : fieldErrors) {
//将所有的异常信息封装成String
stringBuilder.append(item.getDefaultMessage()).append(" ");
}
ReturnResult returnResult = new ReturnResult();
returnResult.setCode(HttpStatus.BAD_REQUEST.value());
returnResult.setMessage(stringBuilder.toString());
return returnResult;
}
}
@RequestParam注解使用Validator校验
上面使用的@RequestBody
注解,将请求参数绑定到bean
上,但是有的情况下,我们的参数可能只是一个简单的数字或者字符串(采用@RequestParam
注解),如果像上面那样将参数包装成对象甚是麻烦,下面我们看一下,怎么样才能使@RequestParam
注解的参数也可以使用Spring Validator
。
配置MethodValidationPostProcessor
@Configuration
public class ValidatorConfiguration {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
//设置validator模式为快速失败
postProcessor.setValidator(validator());
return postProcessor;
}
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast(true)
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
Controller类上添加@Validated , 然后在方法参数内使用注解校验。
@Controller
@Validated
public class TestValidationController {
@RequestMapping(value = "/demo2",method = RequestMethod.GET)
@ResponseBody
public void demo2(@NotBlank(message = "用户姓名不能为空") @RequestParam String name,
@NotNull(message = "年龄不正确") @Range(min = 1,max = 150,message = "年龄不正确") @RequestParam Integer age){
System.out.println(name);
System.out.println(age);
}
}
到此,基本已经可以满足项目中的使用需求了,下面介绍一下其他的常用方法。
其他使用方式
1.在方法内对Model类校验
Model
类 我们还使用上面的 ModelCreateUserRequest
, 在 Spring Bean
中通过 注入Validator
来进行验证。
@Autowired
private Validator validator;
@RequestMapping(value = "/demo3",method = RequestMethod.GET)
@ResponseBody
public void demo3(){
ModelCreateUserRequest modelCreateUserRequest = new ModelCreateUserRequest();
modelCreateUserRequest.setName("王赛超");
modelCreateUserRequest.setIdCardNum("123456789012345678");
modelCreateUserRequest.setAge(0);
Set<ConstraintViolation<ModelCreateUserRequest>> validate = validator.validate(modelCreateUserRequest);
for (ConstraintViolation<ModelCreateUserRequest> model : validate) {
System.out.println(model.getMessage());
}
}
2.对象级联校验
对象的内部包含另一个对象作为属性,在该属性上添加@Valid
,例如 A
类中 包含B
类 在B
属性上添加 @Valid
,校验A
也可以校验B
。
public class A {
@NotBlank(message = "用户姓名不能为空")
private String name;
@NotNull(message="用户年龄不能为空")
@Min(value = 1,message = "年龄不正确")
@Max(value = 150,message = "年龄不正确")
private Integer age;
@NotNull(message = "B不存在")
@Valid
private B b;
}
public class B {
@Range(min = 1000000,message = "你不能没有钱")
private double money;
}
进行级联校验
@Autowired
private Validator validator;
@RequestMapping(value = "/demo4",method = RequestMethod.GET)
@ResponseBody
public void demo4(){
A a = new A();
a.setAge(25);
a.setName("王赛超");
B b = new B();
a.setB(b);
Set<ConstraintViolation<A>> validate = validator.validate(a);
for (ConstraintViolation<A> model : validate) {
System.out.println(model.getMessage());
}
}
3.分组校验
有的时候,我们对一个实体类需要有多种验证方式,在不同的情况下使用不同验证方式,比如 用户的新增 和 修改 操作,唯一的区别就是 插入的时候没有 userid
属性,而修改的时候有userid
。
GroupA、GroupB:
public interface GroupA extends Default {
}
public interface GroupB extends Default {
}
Model类
public class UserInfo {
/** 主键 */
@NotBlank(message = "用户id不存在",groups = {GroupA.class})
private Integer id;
/** 姓名 */
@NotBlank(message = "用户姓名不能为空",groups = {GroupA.class, GroupB.class})
private String name;
/** 身份证号 */
@Pattern(regexp="^(\\d{18}|\\d{17}(\\d{1}|[X|x]))$",message="身份证格式不正确",groups = {GroupA.class, GroupB.class})
private String idCardNum;
/** 年龄 */
@NotNull(message="用户年龄不能为空",groups = {GroupA.class, GroupB.class})
@Range(min = 1,max = 150,message = "年龄必须在[1,150]",groups = {GroupA.class, GroupB.class})
private Integer age;
/** 家庭住址 */
@NotBlank(message = "用户地址不能为空",groups = {GroupA.class, GroupB.class})
private String address;
/** 状态 0 可用 1 不可用 */
@NotBlank(message = "用户状态不能为空",groups = {Default.class})
private String state;
}
如上 UserInfo
所示,3
个分组分别验证字段如下:
GroupA
验证字段 id
,name
,idCardNum
,age
,address
,state
。
GroupB
验证字段 name
,idCardNum
,age
,address
,state
。
Default
验证字段 state
(Default
是Validator
自带的默认分组)。
在controler中的代码如下:
@RequestMapping(value = "/save",method = RequestMethod.POST)
@ResponseBody
public void save(@RequestBody @Validated( { GroupA.class })UserInfo userInfo){
System.out.println(userInfo.toString());
}
@RequestMapping(value = "/update",method = RequestMethod.POST)
@ResponseBody
public void update(@RequestBody @Validated( { GroupB.class })UserInfo userInfo){
System.out.println(userInfo.toString());
}
验证GroupA
请求save
方法,参数为:
{"id":"","name":"","idCardNum":"","age":"","address":"","state":""}
{
"code": 400,
"message": "用户年龄不能为空 身份证格式不正确 用户id不存在 用户地址不能为空 用户状态不能为空 用户姓名不能为空 ",
"data": null
}
验证GroupB
请求update
方法,参数为:
{"id":"","name":"王赛超","idCardNum":"123456789012345678","age":"25","address":"河北邯郸","state":"0"}
控制台打印:
UserInfo(id=null, name=王赛超, idCardNum=123456789012345678, age=25, address=河北邯郸, state=0)
4.定义组序列
默认情况下,无论它们属于哪个组,都不会按特定顺序计算约束。但是,在某些情况下,控制约束的评估顺序很有用。
例如你要开车,首先你要通过驾照的检测,你才可以开车,为了实现这样的验证顺序,您只需要定义一个接口并对其添加@GroupSequence
,定义必须验证组的顺序 ,这样前面组验证不通过的,后面组不进行验证。
@GroupSequence({ GroupA.class, GroupB.class, Default.class })
public interface OrderedChecks {
}
在controler中的代码如下:
@RequestMapping(value = "/demo7",method = RequestMethod.POST)
@ResponseBody
public void demo7(@RequestBody @Validated( { OrderedChecks.class })UserInfo userInfo){
System.out.println(userInfo.toString());
}
后面我们会学习,如何自定义校验方式。