Spring 参数校验

在前端对数据进行校验的情况下,在后端还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。

JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候很方便地进行校验了!

实际校验的时候,我们用的是 Hibernate Validator 框架。

Hibernate Validator 是 Hibernate 团队最初的数据校验框架:

  • Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现;

  • Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现;

  • Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现,是目前最新版的。

SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。

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

需要注意的是:所有的注解,推荐使用 JSR 注解,即 javax.validation.constraints,而不是 org.hibernate.validator.constraints

依赖

导入 maven 的依赖:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.9.Final</version>
</dependency>
<dependency>
    <groupId>javax.el</groupId>
    <artifactId>javax.el-api</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>org.glassfish.web</groupId>
    <artifactId>javax.el</artifactId>
    <version>2.2.6</version>
</dependency>

验证方式

验证 Controller 的输入

验证请求体:RequestBody

我们需要在待验证的参数上加上 @Valid 注解,如果验证失败,它将抛出 MethodArgumentNotValidException。默认情况下,Spring 会将此异常转换为 HTTP Status 400(错误请求)。

【示例】

@RestController
@RequestMapping("/api")
publicclass PersonController {
    @PostMapping("/person")
    public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
        return ResponseEntity.ok().body(person);
    }
}

通常,我们需要一个全局异常处理器,帮助我们捕获未被处理的异常:

@ControllerAdvice(assignableTypes = {PersonController.class})
publicclass GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
    }
}

验证请求参数:Path Variables 和 Request Parameters

此时,我们需要在 Controller 上加上 @Validated 注解,这样才能使 Spring 校验方法参数。

@RestController
@RequestMapping("/api")
@Validated
publicclass PersonController {
    @GetMapping("/person/{id}")
    public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) {
        return ResponseEntity.ok().body(id);
    }

    @PutMapping("/person")
    public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6,message = "超过 name 的范围了") String name) {
        return ResponseEntity.ok().body(name);
    }
}

ExceptionHandler

@ControllerAdvice(assignableTypes = {PersonController.class})
publicclass GlobalExceptionHandler {
    @ExceptionHandler(ConstraintViolationException.class)
    ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
    }
}

校验 Service

只需要在 Service 类上加上 @Validated 注解即可:

@Service
@Validated
publicclass PersonService {
    public void validatePerson(@Valid Person person){
        // do something
    }
}

Validator 编程方式手动进行参数验证

非 spring 项目中,通过 Validator 工厂类获得 Validator 实例,手动校验:

    public void check_person_manually() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Person person = new Person();
        person.setSex("Man22");
        person.setClassId("82938390");
        person.setEmail("SnailClimb");
        Set<ConstraintViolation<Person>> violations = validator.validate(person);
        //output:
        //email 格式不正确
        //name 不能为空
        //sex 值不在可选范围
        for (ConstraintViolation<Person> constraintViolation : violations) {
            System.out.println(constraintViolation.getMessage());
        }
    }

在 spring 项目中,我们也可以通过 @Autowired 直接注入的方式。

@Autowired
Validator validate

自定义 Validator

校验字段范围

【场景】:对于 region 字段只能是 China、China-Taiwan、China-HongKong 这三个中的一个。

接下来,我们按照下面的步骤,就可以实现一个自定义注解校验。

  • 步骤一:创建一个枚举类
enum Region {
    CHINA("China"), TAIWAN("China-Taiwan"), HONGKONG("China-HongKong");

    private final String value;

    Region(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}
  • 步骤二:创建一个注解
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = EnumValidator.class)
@Documented
public @interface EnumSelector {

    Class<? extends Enum<?>> clazz();

    String message() default "the value should be the enumerate value in the class {clazz}";

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

    Class<? extends Payload>[] payload() default {};
}
  • 步骤三:实现 ConstraintValidator 接口,并重写 isValid 方法
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;

public class EnumValidator implements ConstraintValidator<EnumSelector, Object> {
    private List<String> values;

    @Override
    public void initialize(EnumSelector enumSelector) {
        values = Stream.of(enumSelector.clazz().getEnumConstants()).map(Enum::toString).collect(Collectors.toList());
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (Objects.isNull(value)) {
            return true;
        }
        return values.contains(value.toString());
    }
}
  • 步骤四:使用注解:
    @EnumSelector(clazz= Region.class)
    @NotNull
    private String region;

验证组

某些场景下我们需要使用到验证组,例如,对资源操作的不同方法,有不同的验证规则。

  • 先创建三个接口
public interface QueryUser {
}

public interface AddUser {
}

public interface DeleteUser {
}
  • 使用验证组
class User {
    @NotNull(groups = {AddUser.class, DeleteUser.class})
    private String name;

    @NotNull(groups = {QueryUser.class, DeleteUser.class})
    private String userId;
    ...
}
  • 校验
@Service
@Validated
publicclass UserService {
    @Validated(AddUser.class)
    public void addUser(@Valid User user) {
        // do something
    }

    @Validated(QueryUser.class)
    public void getUser(@Valid User user) {
        // do something
    }

    @Validated(DeleteUser.class)
    public void deleteUser(@Valid User user) {
        // do something
    }

}

使用验证组这种方式的时候一定要小心,这是一种反模式,还会造成代码逻辑性变差。


参考:

posted @ 2023-10-19 17:12  LARRY1024  阅读(145)  评论(0编辑  收藏  举报