您的位置:首页 > 编程语言 > Java开发

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

2018-03-26 12:30 701 查看
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=)
集合元素数量必须在
min
max
之间
@CreditCardNumber(ignoreNonDigitCharaters=)
字符串必须是信用卡号(美国标准信用卡)
@Email
字符串必须是Email地址
@Length(min=, max=)
字符串长度必须在
min
max
之间
@NotBlank
字符串必须有字符
@NotEmpty
字符串不为
null
,集合必须有元素
@Range(min=, max=)
数字必须在
min
max
之间
@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
@NotBlank
3个注解的区别:

@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())
设置当前时区为系统默认时区,最后再毫秒化。

Controlle
4000
r
方法为:

@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
提供的校验注解一样,必须拥有
message
groups
payload
三个属性。

第二步: 编写自定义校验的逻辑实体类,这个类必须实现
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 { };

}


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

message
groups
payload
三个属性是必须的,可以参考@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
服务,并通过
Spring
DI
注入到这个校验类中,需要注意的是,这个校验类上并不需要添加
Spring
Component
等注解,
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文档

示例代码下载地址:

项目已经上传到码云,欢迎下载,内容所在文件夹为
chapter003
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐