从零开始学习springmvc(4)——Spring请求参数校验(Hibernate Validator)

【项目地址】 从零开始学习springmvc

如需国际化请结合第5章一起看Spring国际化和全局异常处理

四、Spring请求参数校验(Hibernate Validator)

4.1 JSR-303规范

前端传入的请求参数往往是不可信的,因此对参数进行必要的校验可以有效防护CRLF攻击、SQL注入等。在参数校验方面,java也有一项规范为JSR-303 ,它是JAVA EE 6 中的一项子规范,叫做Bean Validation。JSR-303的规范api为validation-api,类似于servlet-apiannotation-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是基于注解式的实现,需要对某个参数进行校验,只需要在相应参数上添加注解即可。常用注解总结如下(加粗为常用):

  • 非空校验
注解支持的类型校验功能备注
@NullObject可以为null
@NotNullObject不能为null必选参数
@NotEmptyCharSequence、Collection、Map、Array不能为null,且不能为空,length/size必须>0必选参数
@NotBlankCharSequence不能为null,且不能为空,且不能为空字符串必选参数

注意:除上述注解外,下面的数值校验、正则校验等参数为null时,其校验结果为通过。

例如,一个Integer参数被@Max标注

@Max(max = 64)
Integer num;

如果num=null时,@Max(max = 64)校验通过。

  • 数值校验
注解支持的类型校验功能
(通过条件)
@MaxBigDecimal、BigInteger,byte、short、int、long以及包装类<=max
@MinBigDecimal、BigInteger,byte、short、int、long以及包装类>=min
@Range数值类型和String[min, max]
@DecimalMaxBigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类<=value
@DecimalMinBigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类>=value
@NegativeBigDecimal、BigInteger,byte、short、int、long、float、double以及包装类<0
@NegativeOrZeroBigDecimal、BigInteger,byte、short、int、long、float、double以及包装类<=0
@PositiveBigDecimal、BigInteger,byte、short、int、long、float、double以及包装类>0
@PositiveOrZeroBigDecimal、BigInteger,byte、short、int、long、float、double以及包装类>=0
@Digits(integer = 4, fraction = 2)BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类整数位数和小数位数上限
@AssertTrueboolean、Booleantrue
@AssertFalseboolean、Booleanfalse
  • 正则校验和长度、数量校验
注解支持的类型校验功能
@PatternCharSequence匹配指定的正则表达式
@SizeCharSequence、Collection、Map、Arraylength/size必须在[min,max]之间
@LengthString字符串长度范围
  • 时间校验
注解支持的类型校验功能
@FutureBigDecimal、BigInteger、CharSequence、byte, short, int, long及其包装类验证日期为当前时间之后
@FutureOrPresentBigDecimal、BigInteger、CharSequence、byte, short, int, long及其包装类验证日期为当前时间或之后
@PastBigDecimal、BigInteger、CharSequence、byte, short, int, long及其包装类验证日期为当前时间之前
@PastOrPresentBigDecimal、BigInteger、CharSequence、byte, short, int, long及其包装类验证日期为当前时间或之前
  • 其他校验
注解支持的类型校验功能
@EmailCharSequence邮箱地址,需regexp指定校验规则,默认为".*"。如"^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$"
@URLURL地址验证
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设置如下:

  1. SpringValidatorAdaptervalidate()方法

  2. MethodValidationInterceptorinvoke()方法

发送post请求post "/users",进行debug:

  1. 执行SpringValidatorAdaptervalidate()方法

  2. 执行ValidatorImplvalidate()方法

  3. 执行ValidatorImplvalidateInContext()方法,根据上下文对参数进行校验

  4. 将校验结果返回,如果结果有错误,说明校验不通过,抛出异常

  5. 由于校验通过,继续进行Spring的参数校验,MethodValidationInterceptorinvoke()方法

  6. 将校验结果返回,如果结果有错误,说明校验不通过,抛出异常


    所以,同时使用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属性),实现如下:

  1. 定义校验注解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 {};
}

  1. 实现校验器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;
    }
}
  1. 使用,可以直接标注在类上。
@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";
}
  1. 测试

如果觉得有用可以关注一下公众号:
程序猿不加班

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值