本文将介绍在 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 true;
}
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
办法。