1、为什么后台也需要校验呢?
虽然我们在前台js进行了拦截,比如submit总体校验一遍,或者每个form控件blur失去焦点的时候进行了校验,但是
我们服务器接口可能被服务器通过代码(http-client)访问,或者其他的方式跳过浏览器js的校验逻辑,如果后台不进行
校验,那么可能会带来严重的安全问题:比如sql注入,XXS攻击等等安全漏洞。
2、使用Hibernate-validator校验。
这个校验框架可不是我们通常所说的Hibernate数据访问层(dao)框架,它只是一个实现JSR-303标准的一个校验框架。
所谓JSR-303其实就是一个校验api定义,而Hibernate-validator是其标准的实现。就像jdbc是java访问数据库的标准api,
而具体的实现由数据库厂商自己去实现。
废话不多说,直接写个demo:
(1)引入相应的jar包
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.3.4.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.4</version>
</dependency>
关于el的jar包引入是因为Hibernate-validator需要使用到el表达式的功能,至少上述版本是这样的,否则运行时会报错。
如果是在web环境,上述el表达式可以设置<scope>provided<scope>。
(2)写好我们需要校验的javaBean
PersonDto.java和Address.java
package normal.test.spring.bootstrap.validator;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
public class PersonDto {
@NotNull
@Length(max = 10,min = 1,message = "姓名必须在1-10个字符之间")
private String name;
@Min(value = 18,message = "年龄不能小于18")
@Max(value = 100,message = "年龄不能大于100")
private Integer age;
@Valid
private Address address;
@Email
private String email;
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setAddress(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}
}
package normal.test.spring.bootstrap.validator;
import javax.validation.constraints.NotNull;
public class Address {
@NotNull
private String country;
@NotNull
private String province;
private String city;
private String cityDetail;
public void setCity(String city) {
this.city = city;
}
public String getCity() {
return city;
}
public void setCountry(String country) {
this.country = country;
}
public String getCountry() {
return country;
}
public void setProvince(String province) {
this.province = province;
}
public String getProvince() {
return province;
}
public void setCityDetail(String cityDetail) {
this.cityDetail = cityDetail;
}
public String getCityDetail() {
return cityDetail;
}
}
其实上述类就是简单java类,只是将JSR-303定义的一些约束类(Constraint)的注解加入到了各个属性而已。
(3)使用Validator
package normal.test.spring.bootstrap.validator;
import org.junit.Test;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
public class HibernateValidatorTest {
@Test
public void test01(){
// 首先获取ValidatorFactory
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
// 然后获取validator实例
Validator validator = validatorFactory.getValidator();
// 进行校验
PersonDto personDto = new PersonDto();
personDto.setName("1111111111111111111111111");
personDto.setAge(111);
Address address = new Address();
address.setCountry("中国");
personDto.setAddress(address);
personDto.setEmail("111111@com");
Set<ConstraintViolation<PersonDto>> constraintViolationSet = validator.validate(personDto);
// 如果constraintViolationSet为空说明没有任务错误
for (ConstraintViolation<PersonDto> personDtoConstraintViolation : constraintViolationSet) {
System.out.println(personDtoConstraintViolation.toString());
System.out.println(personDtoConstraintViolation.getMessage());
}
// 关闭factory
validatorFactory.close();
}
}
运行截图如下:
由此可见,我们@Valid注解提供了递归校验,这样我们只要在对应的javaBean中写上注解,那么校验起来是非常有效的。
(4)总结各个注解的作用:
这些注解有些是Hibernate-validator自定义的,当然上述描述自己要去实践才行。
3、上述两点只是描述了java开发进行数据校验的标准方式,但是我们开发中往往都会使用spring,那么spring其实也是有自己的校验接口的。
org.springframework.validation.Validator就是spring自己提供的接口,换句话说我们可以使用实现该接口来对某个bean进行校验。
下面是借鉴过来的描述:
Spring 具有Validator
接口,可用于验证对象。 Validator
接口使用Errors
对象工作,因此验证器可以将验证失败报告给Errors
对象。
让我们考虑一个小的数据对象:
public class Person {
private String name;
private int age;
// the usual getters and setters...
}
我们将通过实现org.springframework.validation.Validator
接口的以下两种方法来提供Person
类的验证行为:
supports(Class)
-此Validator
可以验证提供的Class
的实例吗?validate(Object, org.springframework.validation.Errors)
-验证给定的对象,如果发生验证错误,请向给定的Errors
对象注册
实现Validator
非常简单,尤其是当您知道 Spring 框架还提供的ValidationUtils
类时。
public class PersonValidator implements Validator {
/**
* This Validator validates *just* Person instances
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}
如您所见,ValidationUtils
类上的static
rejectIfEmpty(..)
方法用于拒绝'name'
属性(如果它是null
或空字符串)。看看ValidationUtils
javadocs,看看它提供了什么功能,除了前面显示的示例。
虽然可以实现单个Validator
类来验证丰富对象中的每个嵌套对象,但是最好将每个嵌套类的验证逻辑封装在自己的Validator
实现中。 *'rich'*对象的一个简单示例是Customer
,它由两个String
属性(名字和名字)和一个复杂的Address
对象组成。 Address
对象可以独立于Customer
对象使用,因此已实现了不同的AddressValidator
。如果您希望CustomerValidator
重用AddressValidator
类中包含的逻辑而不求助于复制粘贴,则可以在CustomerValidator
中依赖注入或实例化AddressValidator
,并按如下方式使用它:
public class CustomerValidator implements Validator {
private final Validator addressValidator;
public CustomerValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("The supplied [Validator] is " +
"required and must not be null.");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException("The supplied [Validator] must " +
"support the validation of [Address] instances.");
}
this.addressValidator = addressValidator;
}
/**
* This Validator validates Customer instances, and any subclasses of Customer too
*/
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}
其实我们经常写在Controller中的方法参数BindingResult就是Errors的扩展。
下面是我的观点了哈:
上述这种spring的校验接口,相当于每个javaBean都要去写对应的XxxValidator接口,其实是非常不方便的。
于是spring中有一个类就整合了第1点和第2点描述的内容,这个类是LocalValidatorFactoryBean
4、重点了,重点了,重点了 。。。。LocalValidatorFactoryBean
该类其实虽然叫FactoryBean但却不是FactoryBean<T>接口的实例。
它准确来讲是javax.validator.Validator的装饰器,同时又将校验功能适配到org.springframework.validation.Validator接口。
所以,我们在代码中可以@Autowire上述两种校验接口。
但是为何我们会对LocalValidatorFactoryBean如何陌生呢?
是因为:
(1)如果在基于xml配置容器元数据时,xml中配置了<mvc:annotation-driven>,那么spring会默认给我们将LocalValidatorFactoryBean注入到容器中。
而且该实例会跟WebDataBinder关联起来,至于WebDataBinder是啥,这里就不多说了。
(2)基于javaCode的方式配置元数据时。
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public Validator getValidator(); {
// return "global" validator
}
}
也就是说往往都是容器替我们配置了LocalValidatorFactoryBean,当然我们也可以自行配置。
那么在具体的controller中我们需要怎么做呢?
如下copy的图所示:
我们需要在入参中对javaBean标上@Valid注解,然后紧接其后加入BindingResult参数,这样WebDataBinder会将
校验的错误结果放入到上图的result中。
当然,往往我们都是通过异常解析器统一处理BindingException。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BindException.class)
@ResponseBody
public ResultBean validationErrorHandler(BindException ex) throws JsonProcessingException {
//1.此处先获取BindingResult
BindingResult bindingResult = ex.getBindingResult();
//2.获取错误信息
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
//3.组装异常信息
Map<String,String> message = new HashMap<>();
for (FieldError fieldError : fieldErrors) {
message.put(fieldError.getField(),fieldError.getDefaultMessage());
}
//4.将map转换为JSON
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(message);
//5.返回错误信息
return new ResultBean("400",json);
}
}
写到这里,我不知道你搞懂了吗?
总结一下:
在我们项目中,往往需要引入Hibernate-validator的jar包,当然spring-boot由starter引入。
目的是为了引入JSR-303的实现。
我们往往不需要配置自己的org.springframework.validation.Validator实例,是因为我们
通过<mvc:annotation-driven>或者@EnableWebMvc的配置告诉spring做了默认注册,当然
该配置,不止是验证那么简单,还有其他的东西。
如果,标准的注解满足不了我们,可以自己实现对应的org.springframework.validation.Validator或者
扩展@Constraint(具体怎么扩展,不在这里提了)。