后端如何进行参数校验?

184 阅读4分钟

如何进行参数校验

1. 不好的参数校验

  • 对于用户输入的数据来说,不只前端要校验数据,后端也要对数进行校验,比如入参是否可以为空、入参长度是否满足你的期望长度
  • 比如你的数据库长度设置的varchar(16),对方直接来了个36的,那么数据库直接异常
  • 如果以if判断来进行参数校验,代码就会非常的难看
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/add")
    public ResponseEntity<String> add(User user) {
        if(user.getName()==null) {
            return ResponseResult.fail("用户名不可为空!");
        } else if(user.getName().length()<5 || user.getName().length()>15){
            return ResponseResult.fail("用户名长度在 5-15 之间");
        }
        if(user.getAge()< 1 || user.getAge()> 150) {
            return ResponseResult.fail("你敢是乌龟吗?");
        }
        // 参数合法
        return ResponseEntity.ok("success");
    }
}
  • 针对这个问题,Java开者在Java API规范 (JSR303) 定义了Bean校验的标准validation-api,但没有提供实现。
  • hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。
  • Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。

2. 开始

  • 引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • 对bean的属性添加校验注解
  • 可以单独使用一个DTO
  • 每个注解指定message属性,为错误信息提示
  • 有哪些常用的校验?(在下面)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;

    @NotBlank
    private String username;

    @NotBlank
    private String password;

    private String sex;

    @Email
    private String email;

    @Pattern(regexp = "^[0-9]{11}$", message = "手机号格式不正确")
    private String phone;
}
  • 在Controller的方法入参上使用@Valid注解,参数校验的值放在BindingResult中
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping
    public ResponseResult addUser(@Valid User user, BindingResult bindingResult) {

		if (bindingResult.hasErrors()) { // 数据格式出现错误
		    Map<String, String> map = bindingResult.getFieldErrors().stream()
		            .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
		    return new ResponseResult(ResponseStatus.FAIL.getCode(), "数据格式校验失败", map) ;
        }

        log.info("用户信息:{}",user);

        return ResponseResult.success("添加成功!");
    }
}

3. 统一异常处理

  • 在数据校验失败时会发出BindException异常,可以使用spring的统一异常处理器进行处理
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping
    public ResponseResult addUser(@Valid User user) {
        log.info("用户信息:{}",user);
        return ResponseResult.success("添加成功!");
    }
}
@RestControllerAdvice
public class CustomExceptionHandler {

    @ExceptionHandler(BindException.class)
    public ResponseResult validationException(BindException e) {

        Map<String, String> map = e.getBindingResult().getFieldErrors().stream()
                .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        return new ResponseResult(ResponseStatus.FAIL.getCode(), "数据格式校验失败", map) ;
    }

}

4. 分组校验

  • 我们一般在数据增删改查时,通常增加时使用数据库的主键自增,也就是增加时主键必须为null,删改查时主键必须不为null
  • 这里同一bean中出现了两种校验场景,可以使用分组校验
  • 【分组】即为接口,接口中只是作为一个标识,无需任何代码
// 添加时的组
public interface AddValid {
}
// 修改时的组
public interface UpdateValid {
}
  • 设置分组后,校验注解可以指定组
  • 没有设置groups属性的注解,使得分组校验时不会校验
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    @Null(groups = {AddValid.class})
    @NotNull(groups = {UpdateValid.class})
    private Long id;

    @NotBlank(groups = {AddValid.class, UpdateValid.class})
    private String username;

    @NotBlank(groups = {AddValid.class})
//    @NotBlank
    private String password;

    private String sex;

    @Email(groups = {AddValid.class, UpdateValid.class})
    private String email;

    @Pattern(regexp = "^[0-9]{11}$", message = "手机号格式不正确", groups = {AddValid.class, UpdateValid.class})
    private String phone;
}
  • 使用分组校验,形参使用@Validated注解,value属性指定组
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {


    @PostMapping
    public ResponseResult addUser(@Validated({AddValid.class}) User user) {

        log.info("用户信息:{}",user);

        return ResponseResult.success("添加成功!");
    }

    @PutMapping
    public ResponseResult updateUser(@Validated(UpdateValid.class) User user) {

        log.info("用户信息:{}",user);

        return ResponseResult.success("修改成功");
    }
}

5. 常用的校验注解

  • JSR303规范
@AssertFalse            被注释的元素只能为false
@AssertTrue             被注释的元素只能为true
@DecimalMax             被注释的元素必须小于或等于{value}
@DecimalMin             被注释的元素必须大于或等于{value}
@Digits                 被注释的元素数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
@Email                  被注释的元素不是一个合法的电子邮件地址
@Future                 被注释的元素需要是一个将来的时间
@FutureOrPresent        被注释的元素需要是一个将来或现在的时间
@Max                    被注释的元素最大不能超过{value}
@Min                    被注释的元素最小不能小于{value}
@Negative               被注释的元素必须是负数
@NegativeOrZero         被注释的元素必须是负数或零
@NotBlank               被注释的元素不能为空、空白字符串
@NotEmpty               被注释的元素不能为空
@NotNull                被注释的元素不能为null
@Null                   被注释的元素必须为null
@Past                   被注释的元素需要是一个过去的时间
@PastOrPresent          被注释的元素需要是一个过去或现在的时间
@Pattern                被注释的元素需要匹配正则表达式"{regexp}"
@Positive               被注释的元素必须是正数
@PositiveOrZero         被注释的元素必须是正数或零
@Size                   被注释的元素个数必须在{min}和{max}之间
  • hibernate validation是对这个规范的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等
@CreditCardNumber       被注释的元素不合法的信用卡号码
@Currency               被注释的元素不合法的货币 (必须是{value}其中之一)
@EAN                    被注释的元素不合法的{type}条形码
@Email                  被注释的元素不是一个合法的电子邮件地址  (已过期)
@Length                 被注释的元素长度需要在{min}和{max}之间
@CodePointLength        被注释的元素长度需要在{min}和{max}之间
@LuhnCheck              被注释的元素${validatedValue}的校验码不合法, Luhn模10校验和不匹配
@Mod10Check             被注释的元素${validatedValue}的校验码不合法, 模10校验和不匹配
@Mod11Check             被注释的元素${validatedValue}的校验码不合法, 模11校验和不匹配
@ModCheck               被注释的元素${validatedValue}的校验码不合法, ${modType}校验和不匹配  (已过期)
@NotBlank               被注释的元素不能为空  (已过期)
@NotEmpty               被注释的元素不能为空  (已过期)
@ParametersScriptAssert 被注释的元素执行脚本表达式"{script}"没有返回期望结果
@Range                  被注释的元素需要在{min}和{max}之间
@SafeHtml               被注释的元素可能有不安全的HTML内容
@ScriptAssert           被注释的元素执行脚本表达式"{script}"没有返回期望结果
@URL                    被注释的元素需要是一个合法的URL
@DurationMax            被注释的元素必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
@DurationMin            被注释的元素必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}

6. 自定义validation

  • 定义注解
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {TelephoneNumberValidator.class}) // 指定校验器
public @interface TelephoneNumber {
    String message() default "Invalid telephone number";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}
  • 定义校验器
public class TelephoneNumberValidator implements ConstraintValidator<TelephoneNumber, String> {
    private static final String REGEX_TEL = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|13[0-9]\\d{8}|15[1089]\\d{8}";

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        try {
            return Pattern.matches(REGEX_TEL, s);
        } catch (Exception e) {
            return false;
        }
    }
}
  • 使用
@Data
@Builder
@ApiModel(value = "User", subTypes = {AddressParam.class})
public class UserParam implements Serializable {

    private static final long serialVersionUID = 1L;

    @NotEmpty(message = "{user.msg.userId.notEmpty}", groups = {EditValidationGroup.class})
    private String userId;

    @TelephoneNumber(message = "invalid telephone number") // 这里
    private String telephone;

}