Spring Security技术栈开发企业级认证与授权(三)表单校验以及自定义校验注解开发

Hibernate不仅仅为操作数据库提供了解决方案,还为数据校验提供了解决方案——Hibernate Validator。本篇博客将介绍常用的Validator注解的使用以及在Validator不满足实际需求的情况下如何使用自定义Validator来实现数据校验。

一、常见的数据校验注解

首先我们需要在项目的POM文件中添加Hibernate Validator的依赖才可以使用它的数据校验器进行数据校验。由于Spring Boot已经将Hibernate Validator集成到了spring-boot-starter-web包里,所以这里不需要额外引用Hibernate Validator依赖。常用的校验注解下表所示:

注解 说明
@NotNull 值不能为空
@Null 值必须为空
@Pattern(regex=) 字符串必须匹配正则表达式
@Size(min=, max=) 集合元素数量必须在minmax之间
@CreditCardNumber(ignoreNonDigitCharaters=) 字符串必须是信用卡号(美国标准信用卡)
@Email 字符串必须是Email地址
@Length(min=, max=) 字符串长度必须在minmax之间
@NotBlank 字符串必须有字符
@NotEmpty 字符串不为null,集合必须有元素
@Range(min=, max=) 数字必须在minmax之间
@SafeHtml 字符串是安全的HTML
@URL 字符串是合法的URL
@AssertFalse 值必须是false
@AssertTrue 值必须是true
@DecimalMax(value=, inclusive=) 如果inclusive=true,那么值必须大于等于value,如果inclusive=false,那么值必须大于value
@DecimalMin(value=, inclusive=) 如果inclusive=true,那么值必须小于等于value,如果inclusive=false,那么值必须小于value
@Digits(integer=, fraction=) 数字格式检查,integer是指整数部分最大长度,fraction是指小数部分最大长度
@Future 值必须是未来的日期
@Past 值必须是过去的日期
@Max(value=) 值必须小于等于value指定的值,不能注释在字符串类型属性上
@Min(value=) 值必须大于等于value指定的值,不能注释在字符串类型属性上

主要区分下@NotNull@NotEmpty@NotBlank3个注解的区别:

@NotNull 任何对象的value不能为null

@NotEmpty 集合对象的元素不为0,即集合不为空,也可以用于字符串不为null

@NotBlank 只能用于字符串不为null,并且字符串trim()以后length要大于0

其实以上的每个注解都有三个共同的属性,因为他们都遵循JSR 303规范:

String message() default "{org.hibernate.validator.constraints.xxx.message}";

Class<?>[] groups() default { };

Class<? extends Payload>[] payload() default { };
  • message提供校验失败后的错误消息;

  • groups分组验证

  • payload承载元数据

为了测试注解的作用,我在User类的属性上加了部分注解,如下所示:

@NotEmpty(message = "用户名不能为空")
private String username;

@NotEmpty(message = "密码不能为空")
private String password;

@Past(message = "生日必须是过去的日期")
private Date birthday;

这里我写一个创建用户的测试用例和Controller,并人为设置不符合要求的数据,测试用例代码如下:

@Test
public void create3() throws Exception {
    Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    String content = "{\"username\":null,\"password\":null,\"birthday\":" + date.getTime() + "}";
    mockMvc.perform(MockMvcRequestBuilders.post("/user3")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(content))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(3));
}

为了测试效果,我设置的时间不是过去的时间,而是未来一年的时间,LocalDateTime.now()获取当前时间,plusYears(1)加上一年,atZone(ZoneId.systemDefault())设置当前时区为系统默认时区,最后再毫秒化。
Controller方法为:

@PostMapping("/user3")
public User create3(@RequestBody @Valid User user, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        bindingResult.getAllErrors().forEach(error -> System.out.println(error.getDefaultMessage()));
    }
    user.setId(3);
    return user;

}

这里对错误消息进行了循环打印,打印结果是:

用户名不能为空
生日必须是过去的日期
密码不能为空

@Valid注解在数据封装之间会对数据的合法性进行校验,并将校验的错误结果存储在BindingResult对象中。

二、自定义校验注解

以上的所有注解都是Java给我们提供的,其实他们往往只能校验一些简单的值,在实际开发中,我们面临的校验可能会很复杂,所以校验逻辑往往需要我们自己来写,这时候就需要我们自定义校验注解了。接下来我以校验身份证号码的案例来说明如何实现自定义的校验注解。
一般来说,自定义校验注解的开发步骤有以下几步:
第一步: 编写校验注解,但是需要注意的是,自定义的校验注解也得和其他Java提供的校验注解一样,必须拥有messagegroupspayload三个属性。
第二步: 编写自定义校验的逻辑实体类,这个类必须实现ConstraintValidator这个接口,这样才可以被注解用来校验。
第三步: 编写具体的校验逻辑。

编写注解:
package com.lemon.security.web.validator;


import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义身份证号码校验注解
 *
 * @author lemon
 * @date 2018/3/31 下午7:43
 */
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdCardValidator.class)
public @interface IsIdCard {

    String message();

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

}

对上面的代码进行如下解释:

  • messagegroupspayload三个属性是必须的,可以参考@NotNull等注解;

  • @Target注解是指定当前自定义注解可以使用在哪些地方,这里仅仅让他可以使用在方法上和属性上;

  • @Retention指定当前注解保留到运行时;

  • @Constraint指定了当前注解使用哪个类来进行校验。

编写注解校验逻辑类:
package com.lemon.security.web.validator;

import com.lemon.security.web.service.IdCardValidatorService;
import org.springframework.beans.factory.annotation.Autowired;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * 校验注解的校验逻辑
 *
 * @author lemon
 * @date 2018/3/31 下午7:57
 */
public class IdCardValidator implements ConstraintValidator<IsIdCard, String> {

    @Autowired
    private IdCardValidatorService idCardValidatorService;

    /**
     * 校验前的初始化工作
     *
     * @param constraintAnnotation 自定义的校验注解
     */
    @Override
    public void initialize(IsIdCard constraintAnnotation) {
        String message = constraintAnnotation.message();
        System.out.println("用户自定义的message信息是:".concat(message));
    }

    /**
     * 具体的校验逻辑方法
     *
     * @param value   需要校验的值,从前端传递过来
     * @param context 校验器的校验环境
     * @return 通过校验返回true,否则返回false
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return idCardValidatorService.valid(value);
    }
}

对以上代码进行如下解释:

  • 校验身份证合法性的实体类IdCardValidator实现了接口ConstraintValidator,其后面的第一个泛型是指为哪个注解提供校验服务,第二个泛型是指需要校验的值的类型;

  • 它需要实现两个方法,第一个是初始化方法,第二个是校验的逻辑方法,在启动校验方法之前,都会进行初始化,可以在初始化方法中初始一些数据,比如获取用户自定义message内容;第二个方法的第一个参数是需要被校验的值,第二个参数是校验的上下文环境;

  • 一般的开发过程中,往往将校验逻辑抽取成为一个Service服务,并通过SpringDI注入到这个校验类中,需要注意的是,这个校验类上并不需要添加SpringComponent等注解,Spring可以自动将校验逻辑服务类实例对象注入到这个类中。在这里就注入了IdCardValidatorService实现类对象。

编写注解校验逻辑接口和实现类:

接口:

package com.lemon.security.web.service;

/**
 * @author lemon
 * @date 2018/3/31 下午8:11
 */
public interface IdCardValidatorService {

    /**
     * 身份证号校验,支持18位、15位和港澳台的10位
     *
     * @param value 需要被校验的值
     * @return 校验通过返回true,否则返回false
     */
    boolean valid(String value);
}

实现类:

package com.lemon.security.web.service.impl;

import cn.hutool.core.util.IdcardUtil;
import com.lemon.security.web.service.IdCardValidatorService;
import org.springframework.stereotype.Service;

/**
 * @author lemon
 * @date 2018/3/31 下午8:14
 */
@Service
public class IdCardValidatorServiceImpl implements IdCardValidatorService {

    @Override
    public boolean valid(String value) {
        return IdcardUtil.isValidCard(value);
    }
}

这里的校验逻辑采用的是Hutool提供的工具包进行校验的,具体可以参考它的文档
这就完成了自定义校验注解的完整案例编写,接下来进行提供RESTful风格的API进行测试。在测试之前,请在原来的User类上加上idCard属性,并加上@IsIdCard注解。

@IsIdCard(message = "身份证号码必须是大陆的18位或者15位,或者是港澳台的10位")
private String idCard;

测试方法:

@Test
public void create4() throws Exception {
    Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    String content = "{\"username\":null,\"password\":null,\"birthday\":" + date.getTime() + ",\"idCard\":\"12345678\"}";
    mockMvc.perform(MockMvcRequestBuilders.post("/user4")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(content))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(4));
}

Controller方法:

@PostMapping("/user4")
public User create4(@RequestBody @Valid User user, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        bindingResult.getAllErrors().forEach(error -> System.out.println(error.getDefaultMessage()));
    }
    user.setId(4);
    return user;
}

在上面的测试方法中,随便设置了一个不合法的身份证号码,显然是会校验失败的,最后的打印结果是:

用户自定义的`message`信息是:身份证号码必须是大陆的18位或者15位,或者是港澳台的10位
身份证号码必须是大陆的18位或者15位,或者是港澳台的10位
用户名不能为空
生日必须是过去的日期
密码不能为空

请认真思考上面的一个自定义校验注解的流程,可以轻松掌握在后期的开发中,使用注解来实现校验,而不是写许多重复的校验逻辑代码。

Spring Security技术栈开发企业级认证与授权系列文章列表:

Spring Security技术栈开发企业级认证与授权(一)环境搭建
Spring Security技术栈开发企业级认证与授权(二)使用Spring MVC开发RESTful API
Spring Security技术栈开发企业级认证与授权(三)表单校验以及自定义校验注解开发
Spring Security技术栈开发企业级认证与授权(四)RESTful API服务异常处理
Spring Security技术栈开发企业级认证与授权(五)使用Filter、Interceptor和AOP拦截REST服务
Spring Security技术栈开发企业级认证与授权(六)使用REST方式处理文件服务
Spring Security技术栈开发企业级认证与授权(七)使用Swagger自动生成API文档
Spring Security技术栈开发企业级认证与授权(八)Spring Security的基本运行原理与个性化登录实现
Spring Security技术栈开发企业级认证与授权(九)开发图形验证码接口
Spring Security技术栈开发企业级认证与授权(十)开发记住我功能

示例代码下载地址:

项目已经上传到码云,欢迎下载,内容所在文件夹为chapter003

猜你喜欢

转载自blog.csdn.net/lammonpeter/article/details/79696312