【项目地址】 从零开始学习springmvc
如需国际化请结合第5章一起看Spring国际化和全局异常处理
Spring请求参数校验
四、Spring请求参数校验(Hibernate Validator)
4.1 JSR-303规范
前端传入的请求参数往往是不可信的,因此对参数进行必要的校验可以有效防护CRLF攻击、SQL注入等。在参数校验方面,java也有一项规范为JSR-303 ,它是JAVA EE 6 中的一项子规范,叫做Bean Validation。JSR-303的规范api为validation-api
,类似于servlet-api
和annotation-api
。而Hibernate Validator
是JSR-303规范的一套具体实现。简单的说,validation-api是一套规范的接口,Hibernate Validator是其具体的实现。
4.2 引入hibernate-validator
hibernate-validator是validation-api的具体实现,所以我们需要引入validation-api和hibernate-validator两个依赖包。至于版本关系,由于需要集成在spring项目中,对依赖的版本由一定的约束,主要是javax与jakarta的区别参考链接。spring5.3.13引入validation-api2.0.2与hibernate-validator6.1.0.Final依赖可以
<properties>
<validation-api.version>2.0.2</validation-api.version>
<hibernate-validator.version>6.0.17.Final</hibernate-validator.version>
</properties>
<dependencies>
<!--参数校验-->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>${validation-api.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
</dependencies>
4.3 SpringMVC中使用hibernate-validator
4.3.1 配置LocalValidatorFactoryBean(非必选)
spring framework 提供了Bean Validation的功能,想要开启检验功能,需要定义一个验证器Bean——LocalValidatorFactoryBean
。LocalValidatorFactoryBean中有一个属性providerClass
,表明验证器的类。使用hibernate-validator的化需要指定providerClass的值为org.hibernate.validator.HibernateValidator
。即spring会自动生成一个HibernateValidator对象,来实现具体的校验功能。配置完LocalValidatorFactoryBean后,在mvc配置指定validator。
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
</bean>
<!--开启SpringMVC注解驱动-->
<!--指定validator-->
<mvc:annotation-driven validator="validator"/>
实际上,如果保证只引入了HibernateValidator一套校验的具体实现,上述配置也可以不需要,只需要引入4.2章节所需的依赖jar包即可,spring会在启动时默认寻找一套校验器的实现,在后面章节LocalValidatorFactoryBean的源码中会具体介绍。这里为了验证上述结论,在项目中实际并不配置LocalValidatorFactoryBean的bean。但是这里为明确项目中使用了校验功能,且指定Hibernate Validator实现,因此增加此项配置。
4.3.2 hibernate-validator常用注解
hibernate-validator是基于注解式的实现,需要对某个参数进行校验,只需要在相应参数上添加注解即可。常用注解总结如下(加粗为常用):
- 非空校验
注解 | 支持的类型 | 校验功能 | 备注 |
---|---|---|---|
@Null | Object | 可以为null | |
@NotNull | Object | 不能为null | 必选参数 |
@NotEmpty | CharSequence、Collection、Map、Array | 不能为null,且不能为空,length/size必须>0 | 必选参数 |
@NotBlank | CharSequence | 不能为null,且不能为空,且不能为空字符串 | 必选参数 |
注意:除上述注解外,下面的数值校验、正则校验等参数为null时,其校验结果为通过。
例如,一个Integer参数被@Max标注
@Max(max = 64)
Integer num;
如果num=null时,@Max(max = 64)校验通过。
- 数值校验
注解 | 支持的类型 | 校验功能 (通过条件) |
---|---|---|
@Max | BigDecimal、BigInteger,byte、short、int、long以及包装类 | <=max |
@Min | BigDecimal、BigInteger,byte、short、int、long以及包装类 | >=min |
@Range | 数值类型和String | [min, max] |
@DecimalMax | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 | <=value |
@DecimalMin | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 | >=value |
@Negative | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | <0 |
@NegativeOrZero | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | <=0 |
@Positive | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | >0 |
@PositiveOrZero | BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 | >=0 |
@Digits(integer = 4, fraction = 2) | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 | 整数位数和小数位数上限 |
@AssertTrue | boolean、Boolean | true |
@AssertFalse | boolean、Boolean | false |
- 正则校验和长度、数量校验
注解 | 支持的类型 | 校验功能 |
---|---|---|
@Pattern | CharSequence | 匹配指定的正则表达式 |
@Size | CharSequence、Collection、Map、Array | length/size必须在[min,max]之间 |
@Length | String | 字符串长度范围 |
- 时间校验
注解 | 支持的类型 | 校验功能 |
---|---|---|
@Future | BigDecimal、BigInteger、CharSequence、byte, short, int, long及其包装类 | 验证日期为当前时间之后 |
@FutureOrPresent | BigDecimal、BigInteger、CharSequence、byte, short, int, long及其包装类 | 验证日期为当前时间或之后 |
@Past | BigDecimal、BigInteger、CharSequence、byte, short, int, long及其包装类 | 验证日期为当前时间之前 |
@PastOrPresent | BigDecimal、BigInteger、CharSequence、byte, short, int, long及其包装类 | 验证日期为当前时间或之前 |
- 其他校验
注解 | 支持的类型 | 校验功能 |
---|---|---|
CharSequence | 邮箱地址,需regexp指定校验规则,默认为".*"。如"^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$" | |
@URL | URL地址验证 |
4.3.3 如何开启spring的参数校验
由4.3.1和4.3.2章节可以看出,开启spring的参数校验前提条件
- 1、引入参数校验依赖,本文中使用Hibernate Validator;
- 2、配置LocalValidatorFactoryBean,指定校验的provider,此步非必选,spring mvc会默认配置。
但是这里还有第三个条件,就是在对需要的校验对象加上@Valid
或@Validated
注解。
- 3、使用
@Valid
或@Validated
注解开启参数校验;
4.3.4 Post请求体body参数校验(model/dto对象参数校验)
1、简单类型的参数校验
参数校验最常见场景时在post请求中对数据进行参数校验。对于简单类型(byte、char、short、int、long、float、double及其包装类,String等)的参数,一般在对应的参数上使用相应的注解,增加校验规则即可。
例如为如下User实体类增加参数校验,校验id和name两个属性不能为空。
@Getter
@Setter
@ToString
public class User {
@NotBlank
private String id;
@NotBlank
private String name;
}
在对应的Controller方法上,对参数使用@Valid
或@Validated
注解开启参数校验
@RequestMapping(value = "/users", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@ResponseStatus(code = HttpStatus.ACCEPTED)
public String getUser(@RequestBody @Valid User users) {
System.out.println(users);
return "success";
}
验证:
可以看到name=" "时校验不通过,报400错误码。这里如果想定制化错误信息,可以参考下一篇【五、Spring国际化和全局异常处理】,处理异常并国际化错误信息,可以实现自定义参数校验错误信息。
假如参数校验规则修改为:
{
"id":"必选,最大64字符,只能为数字、字母、中划线(-)和下划线(_)",
"name":"非必选,最大64字符,只能为数字、字母、中划线(-)和下划线(_)",
}
则实体类校验规则如下
@Getter
@Setter
@ToString
public class User {
@NotBlank // 参数必选,如果前端不传id字段或为"",则序列化为null或"",校验失败
@Length(max = 64) // 最长64个字符
@Pattern(regexp = "^[a-z0-9A-Z_-]*$")
private String id; // 只能为数字、字母、中划线(-)和下划线(_)
@Length(max = 64) // 最长64个字符
@Pattern(regexp = "^[a-z0-9A-Z_-]*$") // 只能为数字、字母、中划线(-)和下划线(_)
private String name;
}
测试:
2、引用类型的嵌套校验
对于成员属性是引用类型,只在引用类型的属性上面增加校验规则的注解是不生效的!必须在引用类型上增加@Valid注解,使得引用类型内属性的校验规则生效,即嵌套校验效果。
例如,用Address类表示User的地址,Address包含了省份、城市、县区和街道信息,并对这些字段加上简单的校验。
@Getter
@Setter
@ToString
public class Address {
@Length(max = 64)
@Pattern(regexp = "^[a-zA-Z]*$")
private String province;
@Length(max = 64)
@Pattern(regexp = "^[a-zA-Z]*$")
private String city;
@Length(max = 64)
@Pattern(regexp = "^[a-zA-Z]*$")
private String county;
// 最大64字符,支持数字、字母和空格
@Length(max = 64)
@Pattern(regexp = "^[a-z0-9A-Z ]*$")
private String street;
}
User类内存在Address的成员属性,
@Getter
@Setter
@ToString
public class User {
@NotBlank
@Length(max = 64)
@Pattern(regexp = "^[a-z0-9A-Z_-]*$")
private String id;
@Length(max = 64)
@Pattern(regexp = "^[a-z0-9A-Z_-]*$")
private String name;
// 未增加@Valid注解时校验不生效
// @Valid
private Address address;
}
用postman模拟请求,为了验证上一小节的结论:
当字段为null时,除非有非空校验,否则校验规则不生效,请求通过
User对象的address属性只指定street字段,且为了验证street字段的校验尚未生效,增加请求无效字符$
。address的其余属性province、city、county字段均不指定,请求体如下:
{
"id": "00112233",
"name": "ZhangSan",
"address" : {
"street": "7427 West Abraham Lane$"
}
}
postman测试结果,校验规则并未生效
在User类的addresss属性上增加@Valid注解后,校验生效
@Getter
@Setter
@ToString
public class User {
@NotBlank
@Length(max = 64)
@Pattern(regexp = "^[a-z0-9A-Z_-]*$")
private String id;
@Length(max = 64)
@Pattern(regexp = "^[a-z0-9A-Z_-]*$")
private String name;
// 未增加@Valid注解时校验不生效
@Valid
private Address address;
}
postman测试:
非法参数
合法参数,且address的province、city、county字段为null,校验通过
3、body请求体参数校验总结
- 如果没有非空校验,则字段为null校验规则不生效,校验通过。
- 简单类型校验直接在字段上加校验规则,引用类型开启嵌套校验需要使用@Valid注解
- boolean、Boolean、Enum等有特殊取值范围的类型,可以不加校验。因为如果不满足其取值范围(如boolean只能为true或false,枚举只能取枚举值),对象反序列化失败。
- 包装类Boolean、Integer、Double等如果不加非空校验,参数不传时为null,此时校验不生效。
4.3.5 Get请求参数和URL路径参数校验(@RequestParam和@PathVariable)
get请求参数校验与路径参数没法直接像body参数那样生效,想要实现这类参数的校验有两种方案:
- 使用 Spring的校验机制,参考【4.4 Spring的校验机制以及@Validated注解的使用】。此种方法会存在校验两次的问题,导致重复校验。如果body参数不是特别多,校验规则不复杂可以使用此种方法。
- 使用拦截器Interceptor,拦截所有的参数然后进行校验。此种方法参考后续拦截器相关章节。
4.4 Spring的校验机制以及@Validated注解的使用。
上面章节主要介绍的是Hibernate Validator的使用以及Spring对其完美地支持。接下来要介绍的是Spring的校验机制,其校验规范为Spring's JSR-303
。Spring的JSR-303规范是标准JSR-303的一个变种,其不仅仅支持Controller的请求参数校验,还能对java对象(java bean)进行校验。
4.5.1 MethodValidationPostProcessor介绍
MethodValidationPostProcessor是BeanPostProcessor的实现类,也就是Spring的bean后处理器中的一种。它遵守spring的JSR-303规范,能实现方法级别的校验。只要在方法的参数或者返回值加上相关的校验规则(校验注解),并且在类上加上@Validated
注解,就能对参数和返回进行校验。例如:
public class TestValid {
public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2) {
// 方法体
}
}
使用MethodValidationPostProcessor之前必须在application.xml中进行配置
<!--开启Spring自身的校验机制-->
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
4.5.2 使用Spring的校验机制校验get请求参数和路径参数。
Spring的校验机制校验的是被spring托管的bean,由于Controller本身就是一个被Spring托管的bean(存在@Controller注解),所以就可以来校验方法参数。而上一小节也说过,直接在Controller类方法上对get请求参数和路径参数使用校验规则是无效的(可以自行验证)。而路径参数和请求参数对于Controller方法而言都是方法参数,也就不难理解为什么可以使用Spring的校验机制校验这类参数了。
例如,对于请求
-
get "/user/{user_id}"
路径参数:
user_id
,校验规则为最长64字符,数字、字母、下划线和中划线。 -
post "/users"
body参数:User,校验规则如上一小节
@Controller
@Validated // 只有使用@Validated注解,才能使用Spring的方法参数校验机制
public class UserController {
@Resource
private UserService userService;
@RequestMapping(value = "/user/{user_id}", method = RequestMethod.GET, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@ResponseStatus(code = HttpStatus.OK)
public User getUser(
@PathVariable(value = "user_id") @Length(max = 64) @Pattern(regexp = "^[a-z0-9A-Z_-]*$") String id,
@RequestAttribute(value = "token") String tokenInfo) {
System.out.println(tokenInfo);
return userService.getUser(id);
}
@RequestMapping(value = "/users", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@ResponseStatus(code = HttpStatus.ACCEPTED)
public String getUser(@RequestBody @Valid User users) {
System.out.println(users);
return "success";
}
}
postman测试:
-
get请求,
user_id
参数超过64字符,校验生效,响应500响应500原因是参数校验异常,此时spring会抛出
ConstraintViolationException
异常,而有没有对异常处理(后面可以使用全局异常处理),所以默认任务服务器内部错误,响应500。Tomcat日志如下:
4.5.3 Spring的校验机制和Hibernate Validator重复校验问题
由上可以总结出:
- post请求体body的参数校验,使用的是
LocalValidatorFactoryBean
,在Controller请求上有效。 - get请求参数和路径参数校验,使用的是Spring的校验机制
MethodValidationPostProcessor
,是对Spring托管的bean的方法参数的校验。
这很容易就会想到一个问题,对于post请求,body参数也算是方法参数之一,那Spring的校验机制会对body参数进行校验吗?答案是肯定的。
这里预先给出一些类的作用
-
SpringValidatorAdapter
:Spring适配标准JSR-303的适配器,其内部承载了一个Validtor对象,实际校验是调用Validtor#validate()方法
-
MethodValidationInterceptor
:MethodValidationPostProcessor
的实现是切面(AOP)实现的,MethodValidationInterceptor
是方法参数校验功能的实现者 -
ValidatorImpl
:来源自Hibernate Validator,是标准规范JSR-303的定义
validation-api
中的Validator
接口的实现者。所有的校验都由此类中的方法完成。
可以debug一个堆栈信息,breakpoints设置如下:
-
类
SpringValidatorAdapter
的validate()
方法 -
类
MethodValidationInterceptor
的invoke()
方法
发送post请求post "/users"
,进行debug:
-
执行
SpringValidatorAdapter
的validate()
方法 -
执行
ValidatorImpl
的validate()
方法 -
执行
ValidatorImpl
的validateInContext()
方法,根据上下文对参数进行校验 -
将校验结果返回,如果结果有错误,说明校验不通过,抛出异常
-
由于校验通过,继续进行Spring的参数校验,
MethodValidationInterceptor
的invoke()
方法 -
将校验结果返回,如果结果有错误,说明校验不通过,抛出异常
所以,同时使用Spring的校验机制与Hibernate Validator校验Controller类上的post请求body参数,实际上会对该参数校验两次。这就是在【4.3.5 Get请求参数和URL路径参数校验(@RequestParam和@PathVariable)】推荐大家使用拦截器进行校验的原因。
4.5 @Valid与@Validated注解的区别
@Valid | @Validated | |
---|---|---|
来源不同 | JSR-303规范,是validation-api的原生注解 | 是spring校验机制提供的注解 |
注解使用 | 可以在方法、构造函数、方法参数和成员属性(字段)上 | 可以在类、方法、方法参数上使用,不能用在成员属性(字段)上 |
嵌套校验 | 支持 | 不支持(不能用在成员属性上) |
分组校验 | 不支持 标准的JSR-303没有定义分组校验功能 | 支持 Spring的JSR-303规范是标准JSR-303的一个变种, 提供了一个分组功能 |
分组校验使用场景不多,如果想了解的可以自行检索相关资料。
4.6 自定义参数校验器
有时候默认校验器无法满足校验,可以自定义校验实现。自定义校验器实现步骤:
- 定义校验注解:定义一个注解用于标注在待校验的类、方法或者参数上等等
- 实现校验器:校验器的实现,需要实现
ConstraintValidator<A, T>
接口
现对User类有校验约束:当User的id长度大于10时,用户的城市必须以特定前缀开头(user的address中的city属性),实现如下:
- 定义校验注解
UserValid
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {UserValidator.class})
public @interface UserValid {
String message() default "";
String prefix() default "";
/**
* @return the groups the constraint belongs to
*/
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- 实现校验器
UserValidator
public class UserValidator implements ConstraintValidator<UserValid, User> {
private String prefix = "";
private final int MIN = 10;
@Override
public void initialize(UserValid userValid) {
// 初始化可以从注解中获取值
prefix = userValid.prefix();
}
@Override
public boolean isValid(User user, ConstraintValidatorContext context) {
// user为null时跳过校验,返回true
if (user == null) {
return true;
}
boolean isValid = false;
if (user.getId() != null && user.getAddress() == null) {
String id = user.getId();
Address address = user.getAddress();
if (id.length() > MIN) {
isValid = address.getCity().startsWith(prefix);
}
}
if (!isValid) {
String messageTemplate = context.getDefaultConstraintMessageTemplate(); context.buildConstraintViolationWithTemplate(messageTemplate)
.addConstraintViolation()
.disableDefaultConstraintViolation();
}
return isValid;
}
}
- 使用,可以直接标注在类上。
@Getter
@Setter
@ToString
@UserValid(prefix = "Sa")
public class User {
@NotBlank
@Length(max = 64)
@Pattern(regexp = "^[a-z0-9A-Z_-]*$")
private String id;
@Length(max = 64)
@Pattern(regexp = "^[a-z0-9A-Z_-]*$")
private String name;
// 未增加@Valid注解时校验不生效
@Valid
private Address address;
}
UserController上使用@Valid对user对象进行校验
@RequestMapping(value = "/users", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@ResponseStatus(code = HttpStatus.ACCEPTED)
public String getUser(@RequestBody @Valid User users) {
System.out.println(users);
return "success";
}
- 测试
如果觉得有用可以关注一下公众号: