API安全(五)-參數校驗


1、為什么要做數據校驗

  要保證系統的安全性,健壯性,數據校驗必不可少,校驗參數的合法性,不能因為前端或者其它調用段因為參數傳的不對導致我們的系統報錯。

2、開發中參數校驗做在哪里

  一般都是做在接口層面,對傳入的參數進行校驗。

3、Bean Validation

  對於Controller接口的參數校驗,如果參數較少可以自己寫代碼進行校,但是如果參數較多,就會由一堆if-else,代碼不美觀。我們可以使用Bean Validation來進行參數校驗,Bean Validation是一種Java規范,可以允許使用注釋來對對象上的字段進行約束,也可以自定義約束。從1.0(JSR303),1.1(JSR349)發展到現在的2.0(JSR380)。使用Bean Validation 2.0需要Java 8或更高版本,由Hibernate Validator作為提供商。

4、在SpringBoot中使用Bean Validation

  如果我們使用了spring-boot-starter-web依賴,那么就會自動為我們將相關依賴添加進來。如果想單獨使用的話,需要添加spring-boot-starter-validation依賴。

5、Bean Validation中的校驗注解

validation-api中在javax.validation.constraints包下提供了一系列標准校驗注解

  @AssertFalse:值必須是false。支持的類型有boolean和Boolean。null值被認為是合法的。

  @AssertTrue:值必須是true。支持的類型有boolean和Boolean。null值被認為是合法的。

  @DecimalMax:值必須小於等於value指定的值;是否包含value由inclusive控制,默認為包含(true)。支持的類型有BigDecimal、BigInteger、字符串、byte、short、int、long及其包裝類。不支持double和float。null值被認為是合法的。

  @DecimalMin:值必須大於等於value指定的值;是否包含value由inclusive控制,默認為包含(true)。支持的類型有BigDecimal、BigInteger、字符串、byte、short、int、long及其包裝類。不支持double和float。null值被認為是合法的。

  @Digits:必須是指定范圍內的數字。integer指定整數部分最大長度;fraction指定小數部分最大長度。支持的類型有BigDecimal、BigInteger、字符串、byte、short、int、long及其包裝類。null值被認為是合法的。

  @Email:字符串必須是格式正確的電子郵件地址。也可以通過regexp和flag指定自定義的email格式。null值被認為是合法的。

  @Future:必須是未來的時間或日期。支持的類型有Date、Instant、LocalDate、LocalDateTime、LocalTime等常用時間日期類。null值被認為是合法的。

  @FutureOrPresent:必須是當前或未來的時間或日期。支持的類型有Date、Instant、LocalDate、LocalDateTime、LocalTime等常用時間日期類。null值被認為是合法的。

  @Max:值必須小於等於指定的value。支持的類型有BigDecimal、BigInteger、byte、short、int、long及其包裝類。不支持double和float。null值被認為是合法的。

  @Min:值必須大於等於指定的value。支持的類型有BigDecimal、BigInteger、byte、short、int、long及其包裝類。不支持double和float。null值被認為是合法的。

  @Negative:值必須是負數(小於零)。支持的類型有BigDecimal、BigInteger、byte、short、int、long、float、double及其包裝類。null值被認為是合法的。

  @NegativeOrZero:值必須小於等於零。支持的類型有BigDecimal、BigInteger、byte、short、int、long、float、double及其包裝類。null值被認為是合法的。

  @NotBlank:字符串不能為空,並且必須至少包含一個非空白字符。null值不合法。

  @NotEmpty:值不能為空。支持的類型,字符串不能為null且長度不能為0;集合不能為null且不能為空;Map不能為null且不能為空;數組不能為null且不能為空。

  @NotNull:值不能為null。支持任何類型。

  @Null:值必須是null。支持任何類型。

  @Past:必須是過去的時間或日期。支持的類型有Date、Instant、LocalDate、LocalDateTime、LocalTime等常用時間日期類。null值被認為是合法的。

  @PastOrPresent:必須是當前或過去的時間或日期。支持的類型有Date、Instant、LocalDate、LocalDateTime、LocalTime等常用時間日期類。null值被認為是合法的。

  @Pattern:必須與指定的正則表達式匹配。null值被認為是合法的。

  @Positive:值必須是正數(大於零)。支持的類型有BigDecimal、BigInteger、byte、short、int、long、float、double及其包裝類。null值被認為是合法的。

  @PositiveOrZero:值必須大於等於零。支持的類型有BigDecimal、BigInteger、byte、short、int、long、float、double及其包裝類。null值被認為是合法的。

  @Size:個數必須在指定的min和max之間(包括min和max)。支持的類型字符串長度,集合中元素個數,Map中鍵值對個數,數組的長度。null值被認為是合法的。

hibernate-validator中在org.hibernate.validator.constraints包下也提供了一系列校驗注解,常用的如下:

  @Length:字符串長度在指定的min和max之間(包括min和max)。null值被認為是合法的。

  @Range:值必須在適當的范圍min和max之間(包括min和max)。應用於數值或數值的字符串表示形式。null值被認為是合法的。 @URL:字符串是否為URL。也可以通過regexp和flag指定自定義的email格式。null值被認為是合法的。

6、添加用戶使用參數校驗

  6.1、UserDTO上添加校驗注解

/**
 * @author caofanqi
 * @date 2020/1/20 13:08
 */
@Data
public class UserDTO {

    @Null(message = "創建用戶時,id必須為null")
    private Long id;
    
    @NotNull(message = "名稱不能為空")
    private String name;

    @NotBlank(message = "用戶名不能為空")
    private String username;

    @NotBlank(message = "密碼不能為空")
    private String password;

}

  6.2、在接口類上添加@Validated注解

@Validated
@RestController
@RequestMapping("/users")
public class UserController {
......
}

  6.3、在接口上添加@Validated或@Valid注解

    @PostMapping
    public UserDTO create(@RequestBody @Validated UserDTO userDTO){
        return userService.create(userDTO);
    }

  6.4、測試添加用戶,這這時就會對我們的參數進行校驗,如果不符合我們的要求就會報錯

  正確的參數

7、異常結果處理1

  在錯誤參數訪問時,通過控制台打印可以知道會拋出一個org.springframework.web.bind.MethodArgumentNotValidException異常,

  我們對其進行捕獲並處理,使返回結果更友好。

/**
 * 異常處理器
 *
 * @author caofanqi
 * @date 2020/1/27 21:17
 */
@RestControllerAdvice
public class ControllerAdvice {


    /**
     * @param e 參數校驗異常
     * @return 具體不符合規范的參數及其原因
     */
    @ExceptionHandler
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public Map<String, String> exceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException e) {
        return e.getBindingResult().getFieldErrors().stream()
                .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
    }


}

  再次測試錯誤參數情況,返回的結果就比較友好了

8、分組校驗

  對於添加用戶,我們是不需要用戶id的,但是修改用戶是需要用戶id的。這時怎么進行校驗呢?Bean Validation為我們提供了分組功能,其實每一個校驗注解都有一個groups屬性,且默認為空(Default分組)。

  8.1、新建兩個接口,Create、Update並繼承Default接口。(不繼承的話,不會校驗默認分組)

/**
 *  新增分組
 * @author caofanqi
 * @date 2020/1/27 21:39
 */
public interface Create extends Default {
}

/**
 * 修改分組
 * @author caofanqi
 * @date 2020/1/27 21:40
 */
public interface Update extends Default {
}

  8.2、修改UserDTO上id的注解,為其添加分組

    @Null(groups = Create.class,message = "創建用戶時,id必須為null")
    @NotNull(groups = Update.class,message = "修改用戶時,必須有id")
    private Long id;

  8.3、在接口的@Validated上添加分組

    @PostMapping
    public UserDTO create(@RequestBody @Validated(Create.class) UserDTO userDTO){
        return userService.create(userDTO);
    }

    @PutMapping
    public UserDTO update(@RequestBody @Validated(Update.class) UserDTO userDTO) {
        return userService.update(userDTO);
    }

  8.4、測試錯誤參數結果如下

    

9、級聯校驗(對象屬性中的屬性校驗)

  對於屬性是對象的,如果我們只添加了@NotNull注解,它也不會去校驗該對象中的屬性,如果想要進行校驗,需要添加@Valid注解。

  9.1、強行舉例,將手機號放到詳情對象中

/**
 * 用戶詳情,為了測試級聯校驗
 *
 * @author caofanqi
 * @date 2020/1/27 22:39
 */
@Data
public class UserInfoDTO {

    @NotBlank(message = "手機號不能為空")
    @Pattern(regexp="^[1]([3-9])[0-9]{9}$",message="手機號格式不正確")
    private String phone;

}

  9.2、在UserDTO中想要校驗電話號添加@Valid注解

    @Valid
    @NotNull(message = "用戶詳情不能為空")
    private UserInfoDTO userInfo;

  9.3、測試如下

10、自定義注解校驗

  如果給我們提供的校驗注解不能滿足我們的需求時,我們可以自定義校驗注解。

  10.1、創建一個注解,找一個原有校驗注解進行copy,修改@Constraint指定校驗邏輯(可以指定多個),和所需要的屬性即可。

/**
 * 自定義校驗注解,具體的校驗邏輯由@Constraint進行指定,可以指定多個
 *
 * @author caofanqi
 */
@Documented
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = {CustomConstraintValidatorForString.class, CustomConstraintValidatorForList.class})
public @interface CustomConstraint {

    String message() default "自定義校驗注解";

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

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

    int value() default 0;


}

  10.2、創建校驗器,實現ConstraintValidator接口,並指定要校驗的注解和要校驗的類型,initialize可以獲得注解上的屬性,進行初始化;isValid校驗邏輯。

/**
 * 自定義注解校驗器,實現ConstraintValidator接口,並指定要校驗的注解和要校驗的類型,
 * initialize可以獲得注解上的屬性,進行初始化;isValid校驗邏輯。
 *
 * @author caofanqi
 * @version 1.0.0
 * @date 2020/1/27 23:17
 */
@Slf4j
public class CustomConstraintValidatorForString implements ConstraintValidator<CustomConstraint,String> {

    private int value;

    @Override
    public void initialize(CustomConstraint constraintAnnotation) {
        value = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }
        log.info("自定義校驗的參數值是:{},注解上的值是:{}",value,this.value);
        return true;
    }


}

  10.3、測試,控制台打印如下,說明執行了校驗邏輯

11、list中做分組校驗

  如果有批量添加功能,需要對整個list進行校驗那么如下

  11.1、將@Validated(Create.class)注解添加在方法上

  11.2、在List中添加@Valid注解

    @PostMapping("/batch")
    @Validated(Create.class)
    public void batchCreate(@RequestBody  List<@Valid UserDTO> userDTOS){
        userService.batchCreate(userDTOS);
    }

  11.3、測試錯誤參數,有報錯,結果不夠友好

12、異常結果處理2

  錯誤參數訪問時,通過控制台打印可以知道會拋出一個javax.validation.ConstraintViolationException異常,我們對其進行捕獲並處理,使返回結果更友好。

    /**
     * @param e 參數校驗異常2
     * @return 具體不符合規范的路徑及其原因
     */
    @ExceptionHandler
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public Map<Path, String> exceptionHandler(javax.validation.ConstraintViolationException e){
        return e.getConstraintViolations().stream()
                .collect(Collectors.toMap(ConstraintViolation::getPropertyPath, ConstraintViolation::getMessage));
    }

  再次測試錯誤參數情況,返回的結果就比較友好了

13、屬性之間的關聯校驗

  這里我們可以自定義一個類校驗注解,這樣就可以拿到所有的屬性,然后進行校驗。比如說name不能和username相同。

  13.1、自定義類注解

/**
 * 自定義類級別注解
 * @author caofanqi
 */
@Documented
@Target({TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = {UserDTOConstraintValidator.class})
public @interface UserDTOConstraint {

    String message() default "";

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

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

}

  13.2、校驗器

/**
 * 自定義UserDTO類校驗器
 *
 * @author caofanqi
 * @date 2020/1/28 1:36
 */
@Slf4j
public class UserDTOConstraintValidator implements ConstraintValidator<UserDTOConstraint, UserDTO> {


    @Override
    public boolean isValid(UserDTO value, ConstraintValidatorContext context) {

        if (value.getName().equals(value.getUsername())){
            /*
             * 更改錯誤消息
             */
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("名稱不能和用戶名相同")
                    .addPropertyNode("name").addConstraintViolation();

            return false;
        }

        return true;
    }
}

  13.3、在類上添加該注解

@Data
@UserDTOConstraint(message = "default message")
public class UserDTO {
......
}

  13.4、測試

 

 

 

Bean Validation官網:https://beanvalidation.org/

Hibernate Validator官方參考文檔:https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#preface

 

項目源碼:https://github.com/caofanqi/study-security/tree/dev-bean-validation

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM