本文将介绍在 SpringBoot
中使用 Spring Validation
进行接口参数校验的方法。首先解决了为什么要进行接口参数校验、为什么后端还需要进行参数校验、为什么要使用参数校验框架等问题。接着详细介绍了在 SpringBoot
中使用 Spring Validation
进行参数校验的方法,并列出了一些常用注解。最后,本文还解决了当前端发送的请求中有些参数不需要进行校验时应该怎么办的问题。
1. 疑问
1.1. 为什么要进行接口参数校验?
接口参数校验是指对于接口传入的参数进行检查和验证,以确保参数的合法性和正确性。这是确保系统的稳定性、安全性和可靠性的重要措施。
举例
-
假设我们有一个注册接口,用户需要填写用户名、密码和邮箱地址。如果我们没有进行参数校验,那么用户可以随意输入任何内容,包括非法字符和恶意代码,这可能会导致系统崩溃、数据泄露和安全问题。如果我们对参数进行校验,就可以避免这些问题,同时提高系统的可靠性和安全性。
-
另一个例子是支付接口,如果我们没有对金额、订单号和支付渠道进行校验,那么可能会出现支付失败、重复支付或者支付金额错误的情况。通过对参数进行校验,可以避免这些问题,同时提高支付的可靠性和安全性。
总之,接口参数校验是保证系统安全、可靠、稳定的必要措施,可以避免非法输入、恶意攻击和数据泄露等问题。
1.2. 为什么在前端已经校验了请求参数,后端的接口还需要进行参数校验呢,这样不是多此一举吗?
虽然前端已经对请求参数进行了校验,但是前端只是为了提高用户体验和减轻服务器的负担而进行的简单校验,并不能完全保证数据的安全性和正确性。因此,后端也需要对参数进行校验,以确保数据的合法性和正确性,避免恶意攻击和数据泄露等问题。
举例
-
假设我们有一个在线购物平台的后端接口,前端需要传入商品
ID
和购买数量,前端已经进行了简单的校验,比如购买数量必须是正整数。但是,如果一个恶意用户手动构造了一个请求,将购买数量改成负数或者0
,那么前端就无法阻止这个请求,这个非法请求就会到达后端,如果后端没有进行参数校验,就可能导致订单错误、库存错误或者支付异常等问题。因此,后端也需要对参数进行校验,以确保数据的合法性和正确性。 -
另一个例子是登录接口,前端可能会对用户名和密码进行简单的校验,比如密码长度必须大于等于
6
位。但是,如果后端没有对参数进行校验,那么可能会出现SQL
注入、XSS
攻击等问题。比如,一个恶意用户可能会在用户名或密码中加入一些恶意代码,从而窃取用户信息或者破坏系统。通过对参数进行校验,可以避免这些问题,提高系统的安全性和可靠性。
1.3. 我们可以使用 if else
进行参数校验,为什么要使用参数校验框架呢?
虽然使用 if else
进行参数校验可以实现基本的校验功能,但是这种方式存在以下几个问题:
- 代码冗余:如果需要对多个接口进行参数校验,就需要在每个接口中编写相同的校验逻辑,代码冗余,维护成本高。
- 可读性差:如果校验逻辑比较复杂,那么使用
if else
进行校验的代码可读性较差,不利于代码的维护和优化。 - 安全性差:如果使用
if else
进行校验,那么恶意用户可能会利用漏洞绕过校验,从而导致安全问题。
因此,使用参数校验框架可以有效地解决上述问题,具有以下几个优点:
- 简化代码:使用参数校验框架可以简化校验逻辑,减少代码冗余,提高代码的可读性和可维护性。
- 提高安全性:参数校验框架可以避免参数注入、
SQL
注入、XSS
攻击等安全问题,提高系统的安全性和可靠性。 - 提高效率:使用参数校验框架可以提高开发效率,减少开发时间和成本,并且可以方便地进行扩展和定制。
总之,使用参数校验框架可以有效地提高系统的安全性、可靠性和可维护性,减少代码冗余,提高开发效率,是开发过程中不可或缺的一部分。
2. 为什么要使用 Spring Validation
权限校验框架?
有哪些参数校验框架呢?
以下是几个常用的参数校验框架:
Hibernate Validator
:Hibernate Validator
是一个基于Bean Validation
标准的参数校验框架,可以实现对Java Bean
属性的校验,支持多种校验注解和自定义校验规则。Spring Validation
:Spring Validation
是Spring
框架提供的参数校验框架,基于Bean Validation
标准,可以实现对Java Bean
属性和方法参数的校验,支持多种校验注解和自定义校验规则。Apache Commons Validator
:Apache Commons Validator
是一个通用的参数校验框架,支持多种校验规则和自定义校验规则,可以实现对字符串、数字、日期等数据类型的校验。JSR-303
:JSR-303
是Java EE 6
中定义的Bean Validation
标准,提供了一套参数校验规范和API
,可以实现对Java Bean
属性的校验,支持多种校验注解和自定义校验规则。Bean-Validation
:Bean-Validation
是一款轻量级的参数校验框架,基于JSR-303
标准,可以实现对Java Bean
属性的校验,支持多种校验注解和自定义校验规则。
这些框架都可以实现对参数的校验,提供了一套完整的校验规范和 API
,可以有效地避免非法参数和恶意攻击,提高系统的安全性和可靠性。根据不同的业务需求和技术栈,可以选择不同的框架进行参数校验。
为什么使用
Spring Validation
?
- 基于
Bean Validation
标准:Spring Validation
是基于Bean Validation
标准的参数校验框架,可以实现对Java Bean
属性和方法参数的校验,提供了一套完整的校验规范和API
,可以很方便地进行扩展和定制。 - 支持多种校验注解:
Spring Validation
支持多种校验注解,比如@NotNull、@Size、@Min、@Max
等,可以满足不同的校验需求,同时也支持自定义校验注解。 - 集成方便:
Spring Validation
是Spring
框架提供的参数校验框架,与Spring
框架集成非常方便,可以通过简单的配置实现参数校验。 - 可扩展性强:
Spring Validation
提供了很好的扩展性,可以自定义校验注解和校验器,满足不同的校验需求。 - 可读性高:
Spring Validation
的校验注解非常简洁明了,代码可读性高,可以很方便地查看和维护校验逻辑。
总之,使用 pring Validation
参数校验框架可以提高代码的可读性和可维护性,减少代码冗余,提高开发效率,同时还可以有效地避免非法参数和恶意攻击,提高系统的安全性和可靠性。
3. 在 SpringBoot
中使用 Spring Validation
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 定义控制器
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@PostMapping("/add")
public User addUser(@RequestBody @Valid User user) {
// ...
}
}
在 addUser
方法的参数列表中使用 @Valid
注解进行参数校验,@RequestBody
注解用于将请求体中的 JSON
对象自动映射为 Java
对象。
注意
- 使用
@Validated
注解的时候,必须要在方法参数上添加@Valid
注解才能生效。
- 定义实体类
public class User {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
// 省略 getter 和 setter 方法
}
在 User
类的属性上使用校验注解,例如 @NotBlank、@Email
等,message
属性用于指定校验失败时的错误提示信息。
- 定义全局异常处理器
当控制器方法的参数校验失败时,会抛出 MethodArgumentNotValidException
异常,此时可以通过定义全局异常处理器中的 handleMethodArgumentNotValidException
方法来处理该异常。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
StringBuilder errorMsg = new StringBuilder();
for (ObjectError error : allErrors) {
errorMsg.append(error.getDefaultMessage()).append("; ");
}
return Result.fail(errorMsg.toString());
}
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
return Result.fail(e.getMessage());
}
}
在 GlobalExceptionHandler
类中添加对 MethodArgumentNotValidException
异常的处理,将校验失败的错误提示信息封装到 Result
对象中返回给客户端。
- 测试
使用 Postman
或其他工具向 /user/add
接口发送 POST
请求,请求体中需要包含 username、password
和 email
三个参数。如果参数校验失败,接口将返回错误提示信息。例如,当 username
为空时,接口将返回以下错误信息:
{
"code": 1,
"msg": "参数校验失败",
"data": "用户名不能为空; "
}
通过使用注解和异常处理器,可以非常方便地实现参数校验功能,并将校验结果返回给客户端。
4. Spring Validation
的常用注解
Spring Validation
提供了很多注解,用于对参数进行校验。
@NotNull
:用于验证被注释的元素不为null
。
@NotNull(message = "用户名不能为空")
private String username;
@NotBlank
:用于验证字符串必须非空。
@NotBlank(message = "用户名不能为空")
private String username;
@Size
:用于验证字符串、集合、数组的大小。
@Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
private String password;
@Size(min = 1, max = 10, message = "至少选择一个爱好,最多选择10个")
private List<String> hobbies;
@Size(min = 1, max = 3, message = "至少选择一个菜,最多选择3个")
private String[] dishes;
@Min
和@Max
:用于验证数字的最小值和最大值。
@Min(value = 1, message = "年龄必须大于等于1岁")
@Max(value = 150, message = "年龄必须小于等于150岁")
private Integer age;
@DecimalMin
和@DecimalMax
:用于验证浮点数或igDecimal
的最小值和最大值。
@DecimalMin(value = "0.01", message = "价格不能低于0.01元")
@DecimalMax(value = "9999.99", message = "价格不能超过9999.99元")
private BigDecimal price;
@Pattern
:用于验证字符串是否符合正则表达式。
@Pattern(regexp = "^[a-zA-Z0-9]{4,20}$", message = "用户名必须由4-20个字母或数字组成")
private String username;
@Email
:用于验证字符串是否为合法的电子邮件地址。
@Email(message = "请输入正确的邮箱地址")
private String email;
@Valid
:用于标记需要递归校验的对象。
public class User {
@NotBlank(message = "用户名不能为空")
private String username;
@Valid
private Address address;
// getter 和 setter 方法省略
}
public class Address {
@NotBlank(message = "所在城市不能为空")
private String city;
@NotBlank(message = "详细地址不能为空")
private String detail;
// getter 和 setter 方法省略
}
这些注解可以也用于控制器的方法参数校验。
@RestController
@Validated
public class UserController {
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
// 处理请求,创建用户
return ResponseEntity.ok(user);
}
}
在上面的示例中,@Valid
注解用于标记 User
对象需要进行校验。在方法执行时,Spring
会自动根据 User
对象的属性上的校验注解对其进行校验,如果校验不通过,则会抛出 ConstraintViolationException
异常。
需要注意的是,为了使校验注解生效,控制器类需要添加 @Validated
注解。
5. 前端发送的请求,有些参数不需要进行校验怎么办?
前端发送的请求,有些参数不需要进行校验。比如我们更新一个用户,而我们对 JavaBean
的所有参数都进行了校验,那这个请求就被拒绝了,这种请求我们应该怎么办?
当我们在校验接口参数时,有些 JavaBean
参数不需要进行校验,可以使用 Spring Validation
提供的 @Validate
d 和 @Valid
注解结合分组校验来实现。
首先,在 JavaBean
参数上使用校验注解时,可以为注解指定一个分组。示例代码如下:
public class User {
@NotBlank(message = "用户名不能为空", groups = {Create.class, Update.class})
private String username;
@NotBlank(message = "密码不能为空", groups = {Create.class})
private String password;
@NotBlank(message = "手机号不能为空", groups = {Create.class})
private String mobile;
// getter 和 setter 方法省略
}
在上面的示例中,@NotBlank
注解被分为了两个组:Create
和 Update
,对应着创建和更新操作。这样,当我们对 User
对象进行校验时,只需要指定需要校验的组即可。示例代码如下:
@RestController
@Validated
public class UserController {
@PostMapping("/users")
public ResponseEntity<User> createUser(@Validated(Create.class) @RequestBody User user) {
// 处理请求,创建用户
return ResponseEntity.ok(user);
}
@PutMapping("/users/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @Validated(Update.class) @RequestBody User user) {
// 处理请求,更新用户
return ResponseEntity.ok(user);
}
}
在上面的示例中,@Validated
注解用于标记控制器类需要进行校验,并通过 groups
属性指定需要校验的分组。@Validated
注解可以作用在控制器类或方法上。@Validated
注解和 @Valid
注解的区别在于,@Validated
注解可以指定需要校验的分组,而 @Valid
注解不支持分组校验。
这样,当我们在创建用户时,只会校验 username、password 和 mobile
参数,而在更新用户时,只会校验 username
参数,不会对 password
和 mobile
参数进行校验。
6. 当 Spring Vaildation 中的校验注解不满足我们的的需求怎么办?
当 Spring Vaildation 中的校验注解不满足我们的的需求怎么办?
- 比如说请求传递的参数中,其中一个参数只能是
1、2、3
数字中的一个且不能为null
,那么我们应该怎么办呢?
这时候有的人可能会说我们使用 @Pattern(regexp = "[123]", message = "只能是 1 2 3其中一个数字")
注解不就行了。但是 @Pattern
注解只能用来验证字符串。
那这个时候我们可以使用自定义参数校验器来实现这个功能:实现用户评论时,评论类型只能是歌曲、歌单或者MV
- 首先我们定义一个枚举类:
1
代表歌曲,2
代表歌单,3
代表MV
。
public enum CommentTypeEnum {
// 歌曲
SONG_TYPE(1),
// 歌单
SONG_SHEET_TYPE(2),
// MV
MV_TYPE(3);
private final int value;
CommentTypeEnum(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
- 定义自定义注解
@Target
指定了注解的作用范围是字段,
@Retention
指定了注解的生命周期是运行时。
@Constraint(validatedBy = AllowedValuesValidator.class)
,表示该自定义注解需要使用 AllowedValuesValidator
这个类来进行验证。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AllowedValuesValidator.class)
public @interface AllowedValuesConstraint {
String message() default "值必须为[1, 2, 3]其中一个,且不为 null";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
这个自定义注解定义了 message()
方法,用于在验证不通过时输出错误信息;groups()
和 payload()
方法用于声明该注解属于哪个验证组和携带哪些元数据。
- 自定义参数验证器
public class AllowedValuesValidator implements ConstraintValidator<AllowedValuesConstraint, Integer> {
@Override
public void initialize(AllowedValuesConstraint constraintAnnotation) {
// no initialization needed
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
for (CommentTypeEnum allowedValue : CommentTypeEnum.values()) {
if (allowedValue.getValue() == value) {
return true;
}
}
return false;
}
}
这段代码是实现了 AllowedValuesConstraint
注解的验证逻辑,即限制某个字段的取值范围只能是 [1, 2, 3]
中的一个。这里使用了 ConstraintValidator
接口来实现验证逻辑。
AllowedValuesValidator
类实现了 ConstraintValidator<AllowedValuesConstraint, Integer>
接口,其中 AllowedValuesConstraint
表示需要验证的注解类型,Integer
表示需要验证的数据类型。
initialize()
方法用于初始化验证器,这里没有任何初始化逻辑,因此留空即可。
isValid()
方法是实现验证逻辑的核心方法。它接收两个参数,value
表示需要验证的值,context
表示验证器上下文,可以用于设置错误信息等。
在 isValid()
中,首先判断需要验证的值是否为 null
,如果则返回 false
,表示验证不通过。(我们使用在属性上添加 @NotNull
来判断字段是否为空)否则,遍历枚举类型 CommentTypeEnum
的所有值,如果 value
和枚举中的某个值相等,则返回 true
,表示验证通过。否则,返回 false
,表示验证不通过。
- 定义全局异常处理器:
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理参数校验失败时抛出的 MethodArgumentNotValidException 异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidationException(MethodArgumentNotValidException e) {
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
StringBuilder errorMsg = new StringBuilder();
for (ObjectError error : allErrors) {
errorMsg.append(error.getDefaultMessage()).append("; ");
}
return Result.failed(101, errorMsg.toString());
}
/**
* 处理其他异常
*/
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
return Result.failed(e.getMessage());
}
}
在处理这 MethodArgumentNotValidException
异常时,通过 e.getBindingResult().getAllErrors()
获取所有的校验错误信息,并将其封装为一个 Result
对象返回。
测试:
这是实体类:Comments
对应的控制层方法:
使用
apifox
进行测试:设置 type = 0
。
发送请求:
但是发送请求之后,但校验参数失败后,抛出的不是
MethodArgumentNotValidException
异常而是 BindException
异常,走的默认的 handleException
方法。
如何解决:
- 可能是因为你的
MethodArgumentNotValidException
异常被转换成了BindException
异常。MethodArgumentNotValidException
是BindException
的子类,当使用 Spring Boot 自带的校验框架校验参数时,如果校验失败,会抛出MethodArgumentNotValidException
异常。但是,当使用Spring MVC
自带的校验框架校验参数时,会抛出BindException
异常。因此,如果你的异常处理器只处理MethodArgumentNotValidException
异常,那么当抛出BindException
异常时,就无法处理该异常,从而导致使用默认的异常处理方式。
为了解决这个问题,你可以将 MethodArgumentNotValidException
异常和 BindException
异常都包含在异常处理器中:
/**
* 处理参数绑定失败时抛出的 BindException 异常
*/
@ExceptionHandler(BindException.class)
public Result handleBindException(BindException e) {
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
StringBuilder errorMsg = new StringBuilder();
for (ObjectError error : allErrors) {
errorMsg.append(error.getDefaultMessage()).append("; ");
}
return Result.failed(101, errorMsg.toString());
}
重新测试:测试成功
注意
- 如果同时存在多个异常处理方法可以处理同一种异常,那么 Spring Boot 会选择优先级最高的异常处理方法进行处理。在上述代码中,
handleValidationException
方法的优先级比handleBindException
方法高,所以当抛出MethodArgumentNotValidException
异常时,会优先调用handleValidationException
方法进行处理,而不是调用handleBindException
方法。