总结篇-后台参数验证的几种方式
1.前言
参数验证是一个常见的问题,无论是前端还是后台,都需对用户输入进行验证,以此来保证系统数据的正确性。对于web来说,有些人可能理所当然的想在前端验证就行了,但这样是非常错误的做法,前端代码对于用户来说是透明的,稍微有点技术的人就可以绕过这个验证,直接提交数据到后台。无论是前端网页提交的接口,还是提供给外部的接口,参数验证随处可见,也是必不可少的。前端做验证只是为了用户体验,比如控制按钮的显示隐藏,单页应用的路由跳转等等。后端才是最终的保障。总之,一切用户的输入都是不可信的。
2.常见的验证的方式
前端的校验是必须的,这个很简单,因为客户体验。后台的校验更是必须的,关键在于如何与目前我们的分层思想(控制层、业务层、持久层)综合起来考虑。在每层都要进行校验吗?还是只在是某个特定层做就可以了?是否有好的校验框架(如前端的jquery校验框架、springmvc校验框架)?总之校验框架还是有很多的,原理不就是对后端接收的数据进行特定规则的判断,那我们怎么制定规则,有怎么去检验呢?
1、表现层验证:SpringMVC提供对JSR-303的表现层验证;
2、业务逻辑层验证:Spring3.1提供对业务逻辑层的方法验证(当然方法验证可以出现在其他层,但笔者觉得方法验证应该验证业务逻辑);
3、DAO层验证:Hibernate提供DAO层的模型数据的验证(可参考hibernate validator参考文档的7.3. ORM集成)。
4、数据库端的验证:通过数据库约束来进行;
5、客户端验证支持:JSR-303也提供编程式验证支持。
1)通过 if-if 判断
if(string.IsNullOrEmpty(info.UserName))
{
return FailJson("用户名不能为空");
}
逐个对参数进行验证,这种方式最粗暴.如果参数一多,就要写n多的if-if,相当繁琐,更重要的是这部分判断没法重用,另一个方法又是这样判断.。
2) 自定义注解实现参数校验
切面拦截controller方法,然后捕获带@CheckParam注解方法参数实例,最后反射实例校验。
controller:
@RequestMapping(value = "update" )
@ResponseBody
public ResultBean update(@CheckParam User user){
return ResultBean.ok();
}
model:
public class User implements Serializable{
@CheckParam(notNull = true)
private String username;
}
annotation:
@Target(value={ElementType.PARAMETER,ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckParam {
boolean notNull() default false;
}
aspect:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@Component
@Aspect
public class CheckParamAspect {
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void methodPointCut() {}
/**
* 环绕切入方法
**/
@Around("methodPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature msig = (MethodSignature) point.getSignature();
Method method = msig.getMethod();
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
Object[] args = point.getArgs();
for (int i = 0; i < args.length; i++) {
Object obj = args[i];
MethodParameter mp = new MethodParameter(method,i);
mp.initParameterNameDiscovery(u);
GenericTypeResolver.resolveParameterType(mp, method.getClass());//Spring处理参数
//String paramName = mp.getParameterName();//参数名
CheckParam anno = mp.getParameterAnnotation(CheckParam.class);//参数注解
if(anno != null){
check(obj);
}
}
return point.proceed();
}
/**
* 校验成员变量
**/
private void check(Object obj) throws IllegalAccessException {
Class clazz = obj.getClass();
for(Field field : clazz.getDeclaredFields()){
CheckParam cp = field.getAnnotation(CheckParam.class);
if(cp != null){
check(obj,clazz, field,cp);
}
}
}
/**
* 取出注解,校验变量
**/
private void check(Object obj, Class clazz, Field field, CheckParam cp) throws IllegalAccessException {
if(cp.notNull()){
field.setAccessible(true);
Object f = field.get(obj);
if(StringUtils.isEmpty(f)){
throw new IllegalArgumentException("类" + clazz.getName() + "成员" + field.getName() + "检测到非法参数");
}
}
}
}
3.自定义ValidationUtils
表单验证工具类ValidationUtils,依赖包commons-lang
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
public class ValidateUtils {
/**
* @param fields
* @param params
* @return
* 不存在的校验规则:返回true
* 关键字不按要求写:返回true
*/
public static SKResult validate(ValidField[] fields, Map<String, String> params){
try {
for(ValidField field : fields){
String name = field.getName();
String desc = field.getDes();
boolean isValid = field.isValid();
String[] rules = field.getRules();
String value = params.get(name); // 对应请求参数值
if(!isValid){
return new SKResult(true, "");
}
for(String rule : rules){
String[] arr = rule.replaceAll(" ", "").split(":");
String arr1 = arr[0]; // required
String arr2 = arr[1]; // true
switch (arr1) {
case "required": // 必须项 required:true|false
if(Boolean.parseBoolean(arr2)){
if(value==null || value.trim().length()==0){
return new SKResult(false, desc+"不能为空");
}
}
break;
case "number": // 必须输入合法的数字(负数,小数) number:true|false
if(Boolean.parseBoolean(arr2)){
try{
Double.valueOf(value);
}catch(Exception e){
return new SKResult(false, desc+"数值类型不合法");
}
}
break;
default:
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("===ValidField格式不合法,请注意检查!");
return new SKResult(true, "ValidField格式不合法");
}
return new SKResult(true, "校验通过");
}
public static void main(String[] args) {
Map<String, String> params = new HashMap<String, String>();
params.put("username", "18702764599");
params.put("password", "123");
ValidField[] fields = {
new ValidField("username", "手机号", true, new String[]{
"required:true",
"isTel:true"
"min:5"
"max:5"
}),
new ValidField("password", "密码", true, new String[]{
"required:true",
"isPassword:true",
"equalTo:#username"
"max:2"
})
};
SKResult sk = ValidateUtils.validate(fields, params);
System.out.println(sk);
//SKResult [result=true, respMsg=校验通过, obj=null, type=null]
}
}
SKResult :
public class SKResult {
// 返回代码
private boolean result;
// 错误信息
private String respMsg;
private Object obj;
//set.get方法
@Override
public String toString() {
return "SKResult [result=" + result + ", respMsg=" + respMsg + ", obj="
+ obj + ", type=" + type + "]";
}
}
ValidField :
public class ValidField {
/**
* 字段名
*/
private String name;
/**
* 字段描述
*/
private String des;
/**
* 为true必须校验
*/
private boolean isValid = false;
/**
* 校验规则
*/
private String[] rules;
public String[] getRules() {
return rules;
}
public void setRules(String[] rules) {
this.rules = rules;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDes() {
return des;
}
public void setDes(String des) {
this.des = des;
}
public boolean isValid() {
return isValid;
}
public void setValid(boolean isValid) {
this.isValid = isValid;
}
public ValidField(String name, String des, boolean isValid, String[] rules) {
super();
this.name = name;
this.des = des;
this.isValid = isValid;
this.rules = rules;
}
}
4) JSR-303规范,Bean Validation
JSR 303(Java Specification Requests 规范提案)是JAVA EE 6中的一项子规范,一套JavaBean参数校验的标准,叫做Bean Validation。JSR 303用于对Java Bean中的字段的值进行验证,Spring MVC 3.x之中也大力支持 JSR-303,可以在控制器中对表单提交的数据方便地验证。
<!--jsr 303-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<!-- hibernate validator-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.0.Final</version>
</dependency>
package com.example.demo;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.ValidationException;
import javax.validation.Validator;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.util.Iterator;
import java.util.Set;
/**
* @author lanxinghua
* @date 2018/08/05 15:51
* @description
*/
public class ValidateTestClass {
@NotNull(message = "reason信息不可以为空")
@Pattern(regexp = "[1-7]{1}", message = "reason的类型值为1-7中的一个类型")
private String reason;
public void setReason(String reason) {
this.reason = reason;
}
public void validateParams() {
//调用JSR303验证工具,校验参数
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<ValidateTestClass>> violations = validator.validate(this);
Iterator<ConstraintViolation<ValidateTestClass>> iter = violations.iterator();
if (iter.hasNext()) {
String errMessage = iter.next().getMessage();
throw new ValidationException(errMessage);
}
}
}
package com.example.demo;
/**
* @author lanxinghua
* @date 2018/08/05 15:56
* @description
*/
public class ValidateTestClassValidateTest {
public static void main(String[] args) {
ValidateTestClass validateTestClass = new ValidateTestClass();
validateTestClass .setReason(null);
validateTestClass .validateParams(); //调用验证的方法
}
}
5. JSR-303规范,Bean Validation在SSM项目中使用
JSR和Hibernate validator的校验只能对Object的属性进行校验。
5.1 Model 中添加校验注解
public class Book {
private long id;
@NotEmpty(message = "书名不能为空")
private String bookName;
@NotNull(message = "ISBN号不能为空")
private String bookIsbn;
@DecimalMin(value = "0.1",message = "单价最低为0.1")
private doubleprice; // getter setter ....... }
5.2 在controller中使用此校验
@RequestMapping(value = "/book",method = RequestMethod.POST)
public void addBook(@RequestBody @Valid Book book) {
System.out.println(book.toString());
}
5.3 分组验证
对同一个Model,我们在增加和修改时对参数的校验也是不一样的,这个时候我们就需要定义分组验证,步骤如下:
定义两个空接口,分别代表Person对象的增加校验规则和修改校验规则
//可以在一个Model上面添加多套参数验证规则,此接口定义添加Person模型修改时的参数校验规则
public interface PersonAddView {}
public interface PersonModifyView {}
Model上添加注解时使用指明所述的分组
public class Person {
private long id;
/**
* 添加groups 属性,说明只在特定的验证规则里面起作用,不加则表示在使用Deafault规则时起作用
*/
@NotNull(groups = {PersonAddView.class, PersonModifyView.class}, message= "添加、修改用户时名字不能为空",payload = ValidateErrorLevel.Info.class)
@ListNotHasNull.List({
@ListNotHasNull(groups = {PersonAddView.class}, message = "添加上Name不能为空"),
@ListNotHasNull(groups = {PersonModifyView.class}, message = "修改时Name不能为空")})
private String name;
@NotNull(groups = {PersonAddView.class}, message = "添加用户时地址不能为空")
private String address;
@Min(value = 18, groups = {PersonAddView.class}, message = "姓名不能低于18岁")
@Max(value = 30, groups = {PersonModifyView.class}, message = "姓名不能超过30岁")
private int age;
//getter setter 方法......
}
此时启用校验和之前的不同,需要指明启用哪一组规则
/**
* 备注:此处@Validated(PersonAddView.class)表示使用PersonAndView这套校验规则,若使用@Valid 则表示使用默认校验规则,若两个规则同时加上去,则只有第一套起作用
* 修改Person对象
* 此处启用PersonModifyView这个验证规则
*/
@RequestMapping(value = "/person", method = RequestMethod.PUT)
public void modifyPerson(@RequestBody @Validated(value ={PersonModifyView.class}) Person person) {
System.out.println(person.toString());
}
6. Spring validator 方法级别的校验
JSR和Hibernate validator的校验只能对Object的属性进行校验,不能对单个的参数进行校验,spring 在此基础上进行了扩展,添加了MethodValidationPostProcessor拦截器,可以实现对方法参数的校验
public @NotNull UserModel get2(@NotNull @Size(min = 1) Integer uuid) {
//获取 User Model
UserModel user = new UserModel(); //此处应该从数据库获取
return user;
}
7. java开源验证框架OVAL
我发现我们公司dubbo服务暴露的接口用这套框架来验证。
//下单支付预处理
@Validator({@Check(name = "orderDTO", adapter = NotNull.class, message = "订单详情不能为空", errorCode = "10")})
Result<BossOrderDTO> orderPayPrepare(BossOrderDTO orderDTO);
hibernater-validator依赖于validation-api,说明这个框架是实现了bean validation规范的,从测试中也可以看出,既可以使用javax.validation包下的注解来做校验,也可以使用自身的注解;而oval不依赖于validation-api.两者大同小异,实现的原理也差不多. Java开源验证框架Oval是一个可扩展的Java对象数据验证框架,功能强大使用简单,验证规则可通过配置文件、注解等方式进行设置,规则的编写可以使用纯Java、JavaScript 、Groovy 、BeanShell等语言。
<dependency>
<groupId>net.sf.oval</groupId>
<artifactId>oval</artifactId>
<version>1.81</version>
</dependency>
实现Oval实体对象类,用户的年龄和名字进行校验,具体代码如下:
public class OvalTest {
@Min(18)
private int age;
@Length(min = 6, max = 12)
private String name;
public static void main(String[] args) {
OvalTest ovalTest = new OvalTest();
ovalTest.age = 12;
ovalTest.name = "yoodb";
Validator validator = new Validator();
List<ConstraintViolation> ret = validator.validate(ovalTest);
System.out.println(ret);
}
}
JSR提供的校验注解:
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
Hibernate Validator提供的校验注解:
@NotBlank(message =) 验证字符串非null,且长度必须大于0
@Email 被注释的元素必须是电子邮箱地址
@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=,max=,message=) 被注释的元素必须在合适的范围内