前端校验后,为什么需要后端校验
在前面完成数据参数绑定到Controller时,我们可以在@RequestParam注解中做简单的空校验,就是设置required属性为true,以此来指定Controller方法中形参是否必须传入。数据校验是很常见的操作,有前端校验,即用户在前端页面上填写表单时,检查数据的合法性,再传入到后端。到了后端,还可以有后端的数据校验,后端校验通常是在业务逻辑方法,也就是我们的Controller方法里完成,例如在参数列表里使用注解完成数据校验。后端校验的原因是为了安全性考虑,防止别人通过接口乱调用后台的业务逻辑方法,假设在业务方法中不进行数据校验,有可能会被别让绕过前端校验,直接通过后端的接口方法调用,传入大量的脏数据到数据库。
在这篇日志中,总结自己使用集成Hibernate的校验框架, 和Spring MVC的Bean Validation数据校验。Bean Validation数据校验可以检查JavaBean中的数据,接下来先看看校验器的搭建。
配置validation校验器
validator声明和属性配置
上面说到,后端校验通常放在业务逻辑方法中,在Spring MVC里也就是我们的处理器适配器中具体实现方法,通过在形参列表中使用注解,完成数据校验。我的处理器映射器和适配器都是用注解的方式配置,即在Spring MVC的配置文件里使用的annotation-driven标签对,既然后端数据校验发生在处理器适配器中,当然校验器也要在annotation-driven标签里声明了:
<!-- 简写方式 配置注解的处理器映射器和适配器 -->
<!-- 添加名为"validator的校验器" -->
<mvc:annotation-driven validator="validator"></mvc:annotation-driven>
在annotation-driven标签中我们声明一个validator属性,指定了一个id为validator校验器。然后我们就继续在下面配置这个id为“validator”的校验器属性:
<!-- 配置校验器 -->
<bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator" />
<property name="validationMessageSource" ref="messageSource" />
</bean>
LocalValidatorFactoryBean是默认的校验器实现,下面配置的providerClass为继承的Hibernate校验框架,最后validationMessageSource表示的是校验使用的资源文件,里面可以自定义编写当校验出数据不合法时,给出的错误提示。如果不配置这个校验资源文件,会默认使用ValidationMessages.properties。
校验资源文件配置
当我们声明了校验资源文件后,就可以使用自己编写的文件实现自定义错误提示信息。首先在Spring MVC配置文件中配置这个资源文件:
<!-- 配置校验资源文件 -->
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:UserValidationMessages</value>
</list>
</property>
<property name="fileEncodings" value="utf-8" /> <!-- 资源文件编码格式 -->
<property name="cacheSeconds" value="120" /> <!-- 资源文件内容的缓存事件1 -->
</bean>
ReloadableResourceBundleMessageSource类用提供读取资源属性文件,也就是我们的校验资源文件,它需要指定一些属性,资源文件名basenames和资源文件的编码格式fileEncodings和资源文件内容的缓存时间cacheSeconds,在这里我们配置校验资源文件为UserValidationMessages,这是我们自己编写的properties文件:
# 设置校验错误提示信息
user.name.length.error=Please enter a name with a length of 1-20 characters.
user.gender.isEmpty=Please enter gender
在下面具体校验注解中我们才会看到它的使用。
前端数据类型转换
还有一个配置,使用conversionService接口完成将前端客户端的一些数据进行转换后,再传入后端服务器端。为什么需要数据类型转换?你看再前端页面中用户输入的数据通常是基本数据类型,整数或者字符串,但传到后端Controller进行业务逻辑处理时,通常我们是用Java类来保存数据。前面的数据参数绑定我们就可以看到,我们的查询用户页面,用户名和性别在前端页面都是字符串类型,传到后端进行查询时,是要用Java类型保存,再传入Controller方法中。所以有时候需要对前端数据进行转换后,再传入后端。我们可以在annotation-driven标签中,配置conversionService属性:
<mvc:annotation-driven conversion-service="conversionService"
validator="validator">
</mvc:annotation-driven>
然后配置conversionService使用FromattingConversionServiceFactoryBean类,该类可以完成字符串和Date类型的转换。
<!-- 字符串转为Date类型的Java类 -->
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionService-FactoryBean">
</bean>
其实,如果我们不在annotation-driven标签中配置conversion-service属性,还是会默认加载一个ConversionService,而且使用的就是FromattingConversionServiceFactoryBean类,在这里我是想展示出有这么一个配置,可以完成前端的数据类型转换。
测试用例
添加校验注解
来到校验注解配置,在这里就要指定我们要对哪些数据进行校验了,例如我们的Controller方法完成对用户数据的查询,Java实体类是User,要求前端填入的查询数据,姓名长度范围在1-20个字符之间,性别不能为空。那么在User类中我们就可以这么配置:
public class User {
private Integer id;
@Size(min=1, max=20, message="{user.name.length.error}")
private String username;
private String password;
@NotEmpty(message="{user.gender.isEmpty}")
private String gender;
private String email;
private String province;
private String city;
private Date birthday;
private Integer age;
// 省略get()和set()方法
}
在username属性上面,我么使用@Size注解,指定min=1和max=20,校验的错误信息为user.name.length.error。在gender属性上,使用@NotEmpty注解完成非空校验,错误信息为user.gender.isEmpty。由于我们在前面配置了校验资源文件,所以输出的会是我们的自定义错误提示。
最后的配置,就是在我们的Controller方法形参列表中,使用@Validated注解,指定哪些参数数据需要进行校验:
@Controller
@RequestMapping("user")
public class validationControllerTest {
private UserServiceImpl userService = new UserServiceImpl();
@RequestMapping("validationByCondition")
public String queryUserByCondition(Model model,
@Validated User user,
BindingResult bindingResult) {
// 保存校验错误信息
List<ObjectError> ErrorList = null;
if (bindingResult.hasErrors()) {
ErrorList = bindingResult.getAllErrors();
for (ObjectError objectError : ErrorList) {
System.out.println(objectError.getDefaultMessage());
}
}
List<User> userList = null;
if(user == null || (user.getUsername() == null && user.getGender() == null)) {
// 如果查询框中两个查询条件都为空,则默认查询所有顾客数据
userList = userService.queryUserList();
} else {
// 否则进行条件查询
userList = userService.queryUserByCondition(user);
}
// model数据传到页面
model.addAttribute("userList", userList);
model.addAttribute("ErrorList", ErrorList);
return "users/validateUser"; // 返回视图
}
}
可以看到,我们要对传入方法的User类实例进行数据校验,并且传入BindingResult类对象,它存放了校验错误信息。第9行首先定义一个ErrorList来存放校验错误信息。第11行获取所有的错误信息放到ErrorList中,然后做输出,如果只是这样输出,错误信息是显示在了控制台上,所以第29行我们通过model对象,将这个ErrorList传到前端页面去显示。
前端视图
为了在前端页面中显示错误信息提示,我们通过model对象把错误信息传到了前端视图中显示:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用户查询列表</title>
</head>
<body>
<form action="validationByCondition.action" method="post">
用户名:<input type="text" name="username" />
性别:<input type="text" name="gender" />
<input type="submit" value="查找" />
</form>
<!-- 显示错误信息 -->
<c:if test="${ErrorList != null}">
<c:forEach items="${ErrorList}" var="error">
<font color="red">${error.defaultMessage}</font><br/>
</c:forEach>
</c:if>
<hr/>
<h2>搜索结果</h2>
<table width="300px;" border=1>
<tr>
<td>顾客名</td>
<td>性别</td>
<td>电子邮箱</td>
<td>省会</td>
<td>城市</td>
</tr>
<c:forEach items="${userList }" var="user">
<tr>
<td>${user.username }</td>
<td>${user.gender }</td>
<td>${user.email }</td>
<td>${user.province }</td>
<td>${user.city }</td>
</tr>
</c:forEach>
</table>
</body>
</html>
第15行我们接收传来的ErrorList,并遍历错误信息,将其显示出来:
Bean Validation分组校验
使用场景
分组校验的使用场景是这样的,假设我们的多个Controller方法都用到了同一个Java类作为数据存储并传入,我们需要对类中哪些属性进行校验,是在实体类里面使用注解标识的,但是如果我们一个Controller方法只需对类中的某一个属性做校验,而另一个Controller方法需要对类中的两个属性做校验,怎么办?方法是使用分组校验,在Java实体类中的属性里标注分组信息,然后在不同的Controller方法中形参列表里的@Validated注解添加分组属性,根据不同的分组,实现不同的校验。
配置分组信息
首先创建两个空接口,用来标识不同的分组:
然后在Java实体类中,为需要校验的属性username和gender,在其注解里添加分组:
public class User {
private Integer id;
@Size(min=1, max=20, message="{user.name.length.error}",
groups= {UserGroup1.class})
private String username;
private String password;
@NotEmpty(message="{user.gender.isEmpty}", groups= {UserGroup2.class})
private String gender;
private String email;
private String province;
private String city;
private Date birthday;
private Integer age;
//省略get()和set()方法
}
我们将username属性的校验放在了UserGroup1,gender属性的校验放在了UserGroup2。
最后,我们在Controller方法里,根据不同的业务逻辑,配置校验分组,实现对共用数据类型的不同校验方式:
@Controller
@RequestMapping("user")
public class validationControllerTest {
private UserServiceImpl userService = new UserServiceImpl();
@RequestMapping("validationByCondition")
public String queryUserByCondition(Model model,
@Validated(value=UserGroup1.class) User user,
BindingResult bindingResult) {
// 保存校验错误信息
List<ObjectError> ErrorList = null;
if (bindingResult.hasErrors()) {
ErrorList = bindingResult.getAllErrors();
for (ObjectError objectError : ErrorList) {
System.out.println(objectError.getDefaultMessage());
}
}
List<User> userList = null;
if(user == null || (user.getUsername() == null && user.getGender() == null)) {
// 如果查询框中两个查询条件都为空,则默认查询所有顾客数据
userList = userService.queryUserList();
} else {
// 否则进行条件查询
userList = userService.queryUserByCondition(user);
}
// model数据传到页面
model.addAttribute("userList", userList);
model.addAttribute("ErrorList", ErrorList);
return "users/validateUser"; // 返回视图
}
}
在@Validated注解中,添加UserGroup1分组,这样,在调用该方法进行业务逻辑处理时,只会对该分组标识的username属性进行校验:
完整实现GitHub已上传:
https://github.com/justinzengtm/SSM-Framework/tree/master/SpringMVC_Project