总结篇-后台参数验证的几种方式

时间:2024-03-10 14:42:00

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=) 被注释的元素必须在合适的范围内

原文地址:https://blog.csdn.net/m0_37499059/article/details/81431562