1. 程式人生 > >Springboot2(12)輕鬆搞定資料驗證

Springboot2(12)輕鬆搞定資料驗證

原始碼地址

對於任何一個應用而言,客戶端做的資料有效性驗證都不是安全有效的,而資料驗證又是一個企業級專案架構上最為基礎的功能模組,這時候就要求我們在服務端接收到資料的時候也對資料的有效性進行驗證。為什麼這麼說呢?往往我們在編寫程式的時候都會感覺後臺的驗證無關緊要,畢竟客戶端已經做過驗證了,後端沒必要在浪費資源對資料進行驗證了,但恰恰是這種思維最為容易被別人鑽空子。畢竟只要有點開發經驗的都知道,我們完全可以模擬HTTP請求到後臺地址,模擬請求過程中傳送一些涉及系統安全的資料到後臺,後果可想而知

文章目錄

JSR-303 註釋介紹

這裡只列舉了javax.validation包下的註解,包含了hibernate-validator驗證包中的常用註解

註解 說明
@NotNull 限制必須不為null
@NotEmpty 驗證註解的元素值不為 null 且不為空(字串長度不為0、集合大小不為0)
@NotBlank 驗證註解的元素值不為空(不為null、去除首位空格後長度為0),不同於@NotEmpty,@NotBlank只應用於字串且在比較時會去除字串的空格
@Pattern(value) 限制必須符合指定的正則表示式
@Size(max,min) 限制字元長度必須在 min 到 max 之間(也可以用在集合上)
@Email 驗證註解的元素值是Email,也可以通過正則表示式和flag指定自定義的email格式
@Max(value) 限制必須為一個不大於指定值的數字
@Min(value) 限制必須為一個不小於指定值的數字
@DecimalMax(value) 限制必須為一個不大於指定值的數字
@DecimalMin(value) 限制必須為一個不小於指定值的數字
@Null 限制只能為null(很少用)
@AssertFalse 限制必須為false (很少用)
@AssertTrue 限制必須為true (很少用)
@Past 限制必須是一個過去的日期
@Future 限制必須是一個將來的日期
@Digits(integer,fraction) 限制必須為一個小數,且整數部分的位數不能超過 integer,小數部分的位數不能超過 fraction (很少用)

Hibernate Validator拓展的註解

註解 說明
@Email 校驗郵件地址
@CreditCardNumber 校驗信用卡號碼
@Length(min=, max=) 功能同@Size,但是隻支援String型別
@URL 字串是否合法的URL
@Range(min=,max=,message=) 被註釋的元素必須在合適的範圍內

hibernate validator校驗demo

實體類

@Getter
@Setter
@NoArgsConstructor
public class DemoModel {
    @NotBlank(message="使用者名稱不能為空")
    private String userName;

    @NotBlank(message="年齡不能為空")
    @Pattern(regexp="^[0-9]{1,2}$",message="年齡不正確")
    private String age;

    @AssertFalse(message = "必須為false")
    private Boolean isFalse;
    /**
     * 如果是空,則不校驗,如果不為空,則校驗
     */
    @Pattern(regexp="^[0-9]{4}-[0-9]{2}-[0-9]{2}$",message="出生日期格式不正確")
    private String birthday;
}

hibernate的校驗模式

1、普通模式(預設是這個模式)

普通模式(會校驗完所有的屬性,然後返回所有的驗證失敗資訊)

2、快速失敗返回模式

快速失敗返回模式(只要有一個驗證失敗,則返回)

兩種驗證模式配置方式:(參考官方文件

failFast:true 快速失敗返回模式 false 普通模式

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .failFast( true )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

和 (hibernate.validator.fail_fast:true 快速失敗返回模式 false 普通模式)

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .addProperty( "hibernate.validator.fail_fast", "true" )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

hibernate的校驗實現

@Configuration
public class ValidatorConfiguration {
    @Bean
    public Validator validator(){
        ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
                .configure()
                .addProperty( "hibernate.validator.fail_fast", "true" )
                .buildValidatorFactory();
        Validator validator = validatorFactory.getValidator();

        return validator;
    }
}

1、請求引數校驗 @Valid

在@RequestBody DemoModel demo之間加註解 @Valid,然後後面加BindindResult即可; BindingResult是驗證不通過的結果集合 。

@RestController
public class VaildController {
    @RequestMapping("/vaild")
    public String vaild(@RequestBody(required = false) @Valid  VaildModel model,
                        BindingResult result){
        if(result.hasErrors()){
            for (ObjectError error : result.getAllErrors()) {
                System.out.println(error.getDefaultMessage());
            }
        }
        System.out.println(JSON.toJSONString(result.getAllErrors()));
        return "success";
    }
}

請求引數

POST請求傳入的引數:{"userName":"dd","age":120,"isFalse":true,"birthday":"21010-21-12"}

2、GET引數校驗(@RequestParam引數校驗)

使用@Valid註解,對RequestParam對應的引數進行註解,是無效的,需要使用@Validated註解來使得驗證生效。

方法所在的Controller上加註解@Validated

@RestController
@Validated
public class ValidationController {

    /**如果只有少數物件,直接把引數寫到Controller層,然後在Controller層進行驗證就可以了。*/
    @RequestMapping(value = "/vaild2", method = RequestMethod.GET)
    public String vaild2(@Range(min = 1, max = 9, message = "年級只能從1-9")
                      @RequestParam(name = "grade", required = true)
                              int grade,
                      @Min(value = 1, message = "班級最小隻能1")
                      @Max(value = 99, message = "班級最大隻能99")
                      @RequestParam(name = "classroom", required = true)
                              int classroom) {
        System.out.println(grade + "," + classroom);
        return grade + "," + classroom;
    }

    /**
     * 驗證不通過時,丟擲了ConstraintViolationException異常,使用同一捕獲異常處理
     */
    @ControllerAdvice
    @Component
    class GlobalExceptionHandler {
        @ExceptionHandler(ConstraintViolationException.class)
        @ResponseBody
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public String handle(ValidationException exception) {
            List<String> errorMsg = new ArrayList<>();
            if(exception instanceof ConstraintViolationException){
                ConstraintViolationException exs = (ConstraintViolationException) exception;

                Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
                for (ConstraintViolation<?> item : violations) {
                    /**列印驗證不通過的資訊*/
                    System.out.println(item.getMessage());
                    errorMsg.add(item.getMessage());
                }
            }
            return "bad request, " +JSON.toJSONString(errorMsg);
        }
    }
}

3、model校驗

	@Autowired
    private Validator validator;

    @RequestMapping("/vaild3")
    public String vaild3(@RequestBody(required = false)  VaildModel vaildModel){
        Set<ConstraintViolation<VaildModel>> violationSet = validator.validate(vaildModel);
        for (ConstraintViolation<VaildModel> model : violationSet) {
            System.out.println(model.getMessage());
        }
        return "success";
    }

4、物件級聯校驗

物件內部包含另一個物件作為屬性,屬性上加@Valid,可以驗證作為屬性的物件內部的驗證:(驗證Demo2示例時,可以驗證Demo2的欄位)

@Data
public class Demo2 {
    @Size(min = 3,max = 5,message = "list的Size在[3,5]")
    private List<String> list;

    @NotNull
    @Valid
    private Demo3 demo3;
}

@Data
public class Demo3 {
    @Length(min = 5, max = 17, message = "length長度在[5,17]之間")
    private String extField;
}

5、分組校驗

有這樣一種場景,新增使用者資訊的時候,不需要驗證userId(因為系統生成);

修改的時候需要驗證userId,這時候可用使用者到validator的分組驗證功能

建立分組

public interface GroupA {
}

public interface GroupB {
}
@Data
public class Person {
    @NotBlank(message = "不能為空",groups = {GroupA.class})
    /**使用者id*/
    private Integer userId;

    @Length(min = 4,max = 20,message = "必須在[4,20]",groups = {GroupB.class})
    @Length(min = 1,max = 3,message = "必須在[1,3]",groups = {GroupA.class})
    /**使用者名稱*/
    private String userName;

}
 	@RequestMapping("/vaild4")
    public String vaild4(@RequestBody @Validated({GroupA.class,Default.class}) Person person,
                         BindingResult result){
        if(result.hasErrors()){
            for (ObjectError error : result.getAllErrors()) {
                System.out.println(error.getDefaultMessage());
            }
        }
        System.out.println(JSON.toJSONString(result.getAllErrors()));
        return "success";
    }	

自定義驗證器

一般情況,自定義驗證可以解決很多問題。但也有無法滿足情況的時候,此時,我們可以實現validator的介面,自定義自己需要的驗證器。

實現了一個自定義的大小寫驗證器

public enum CaseMode {
    UPPER,
    LOWER;
}


@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase {
    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    CaseMode value();
}


public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
    private CaseMode caseMode;
    public void initialize(CheckCase checkCase) {
        this.caseMode = checkCase.value();
    }

    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (s == null) {
            return true;
        }

        if (caseMode == CaseMode.UPPER) {
            return s.equals(s.toUpperCase());
        } else {
            return s.equals(s.toLowerCase());
        }
    }
}