赞
踩
当我们想提供可靠的 API 接口,对参数的校验,以保证最终数据入库的正确性,是必不可少的活。前、后端校验都是保证参数的准确性的手段之一,前端校验并不安全,任何人都可以通过接口来调用我们的服务,就算加了一层token的校验,有心人总会转空子,来传各式各样错误的参数,如果后端不校验,导致数据库数据混乱。传统的方式使用if-else
进行参数校验太太繁琐了,如果有几十个字段要校验,那这个方法里面将会变得非常臃肿,实在不够优雅。
虽然使用if else
进行参数校验可以实现基本的校验功能,但是这种方式存在以下几个问题:
if else
进行校验的代码可读性较差,不利于代码的维护和优化。if else
进行校验,那么恶意用户可能会利用漏洞绕过校验,从而导致安全问题。因此,使用参数校验框架可以有效地解决上述问题,具有以下几个优点:
总之,使用参数校验框架可以有效地提高系统的安全性、可靠性和可维护性,减少代码冗余,提高开发效率,是开发过程中不可或缺的一部分。
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 参数校验框架可以提高代码的可读性和可维护性,减少代码冗余,提高开发效率,同时还可以有效地避免非法参数和恶意攻击,提高系统的安全性和可靠性。
validation
包下其它常用的校验注解:
注意: 字段上面的注解千万不要用错了,不然会报内部错误
注解 | 含义 |
---|---|
@Null | 任何类型必须为null。 |
@NotBlank | 字符串、字符类不能为null,并且去掉空格后长度大于0。 |
@NotNull | 任何类型不能为null。 |
@Length(min = 6, max = 8, message = “密码长度为6-8位。”) | 字符串的长度必须在指定的范围内。 |
@NotEmpty | 适用于String、Collection集合、Map、数组等,参数不能为null且长度大于0。 |
@AssertTrue | Boolean、boolean属性必须是true。 |
@AssertFalse | Boolean、boolean属性必须是false。 |
@Min(10) | 必须是一个数字,且其值必须大于等于指定的最小值(整型)。 |
@Max(10) | 必须是一个数字,且其值必须小于等于指定的最大值(整型)。 |
@DecimalMin(“10”) | 必须是一个数字,且其值必须大于等于指定的最小值(字符串,可以是小数)。 |
@DecimalMax(“10”) | 必须是一个数字,且其值必须小于等于指定的最大值(字符串,可以是小数)。 |
@Size(max = 10, min = 1) | 限定集合的大小必须在指定范围内。 |
@Digits(integer = 3, fraction = 2, message = “请输入有效的数字”) | 用于验证数字的整数位数和小数位数限制。 |
@Past | 时间、日期必须是一个过去的时间或日期。 |
@Future | 时间、日期必须是一个未来的时间或日期。 |
字符串必须是一个有效的邮箱格式。 | |
@Pattern(regexp = “[a-zA-Z]*”, message = “密码不合法”) | 字符串、字符必须匹配指定的正则表达式。 |
@Range(max = 150, min = 1, message = “年龄范围应该在1-150内。”) | 数字类型(原子和包装)必须在指定的范围内。 |
@URL(protocol=, host=, port=, regexp=, flags=) | 被注释的字符串必须是一个有效的URL。 |
@CreditCardNumber | 被注释的字符串必须通过Luhn校验算法,通常用于银行卡、信用卡等号码。 |
@ScriptAssert(lang=, script=, alias=) | 要求存在支持Java Scripting API(JSR 223)的实现。 |
@SafeHtml(whitelistType=, additionalTags=) | 要求classpath中存在jsoup包,用于验证HTML内容的安全性。 |
@Valid(javax.validation包下) 和 @Validated(org.springframework.validation.annotation包下)注解。两者大致有以下的区别:
名称 | 是否实现声明式校验 | 是否支持嵌套校验 | 是否支持分组校验 |
---|---|---|---|
@Valid | false | true | false |
@Validated | true | false | true |
绝大多数场景下,我们使用 @Validated 注解即可。而在有嵌套校验的场景,我们使用 @Valid 注解添加到成员属性上。
@Valid
是 JSR-303/JSR-349 Bean Validation API 的一部分,该 API 被广泛称为 Bean Validation 或 Hibernate Validator(尽管 Hibernate Validator 是该 API 的一个流行实现)。@Valid
不能用在类型级别(如类级别)进行组验证,尽管可以通过一些配置或自定义逻辑来实现。@Validated
是 Spring Framework 提供的注解,用于扩展 @Valid
的功能。@Validated
还支持分组验证,允许你根据不同的上下文应用不同的验证规则。@Validated
相对于 @Valid
的主要优势。通过定义不同的验证组,可以在不同的情况下应用不同的验证规则。@Valid
来自 JSR-303/JSR-349 Bean Validation API,而 @Validated
是 Spring 特有的。@Validated
支持分组验证,而 @Valid
不直接支持(尽管可以通过其他方式实现)。@Valid
通常就足够了。但是,如果你需要更复杂的验证逻辑,比如分组验证,那么 @Validated
是更好的选择。在大多数基本情况下,@Valid
和 @Validated
可以互换使用,因为它们都触发了 Bean 验证。然而,当你需要利用分组验证或其他 Spring 特有的验证功能时,@Validated
提供了更多的灵活性和控制。因此,在选择使用哪个注解时,你应该根据你的具体需求和上下文来决定。
参数校验分为简单校验、嵌套校验、分组校验。
SpringBoot Validation 快速失败是指在使用Spring Boot进行参数校验时,当遇到第一个校验失败的情况时,立即停止后续的校验,并抛出异常。这种机制有助于减少不必要的资源消耗,并使得错误信息更加明确和易于处理。以下是关于SpringBoot Validation 快速失败的一些关键点和实现方式:
在使用Spring Boot进行开发时,通常会使用Hibernate Validator(或Spring的spring-boot-starter-validation)来进行参数校验。默认情况下,如果一个对象中有多个字段需要校验,并且这些字段使用了多个校验注解(如@NotNull、@Max等),Hibernate Validator会按顺序依次校验这些字段。如果某个字段校验失败,它不会立即停止校验,而是会继续校验其他字段,直到所有字段都被校验完毕。这可能会导致以下问题:
为了解决上述问题,可以配置Hibernate Validator以启用快速失败模式(failFast)。这样,当遇到第一个校验失败时,就会立即停止后续的校验。以下是几种实现方式:
@Configuration public class ValidatorConfig { // 第一种 @Bean @ConditionalOnMissingBean(Validator.class) public LocalValidatorFactoryBean validator() { LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); factoryBean.getValidationPropertyMap().put(BaseHibernateValidatorConfiguration.FAIL_FAST, Boolean.TRUE.toString()); return factoryBean; } // 第二种 @Bean public Validator validator(AutowireCapableBeanFactory springFactory) { try (ValidatorFactory factory = Validation.byProvider(HibernateValidator.class) .configure() // 快速失败 .failFast(true) // 解决 SpringBoot 依赖注入问题 .constraintValidatorFactory(new SpringConstraintValidatorFactory(springFactory)) .buildValidatorFactory()) { return (Validator) factory.getValidator(); } } }
新建项目,导入以下依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.6</version> </dependency> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>
在项目的根目录下面创建utils
包,并在此包下面创建全局统一的返回结果对象Results
类
/** * 用于返回 * @param <T> */ @ApiModel("统一返回类") public class Results<T> { public static final String ERROR = "500"; public static final String SUCCESS = "200"; /** * 返回码 */ @ApiModelProperty("返回码,正确码为:200") private String resCode ; /** * 返回消息 */ @ApiModelProperty("返回消息") private String msg ; /** * 返回实体 */ @ApiModelProperty("返回实体") private T obj; public static <T> Results<T> success(){ return success(SUCCESS,"成功",null); } public static <T> Results<T> success(String msg){ return success(SUCCESS,msg,null); } public static <T> Results<T> success(T obj){ return success(SUCCESS,"成功",obj); } public static <T> Results<T> success(String msg,T obj){ return success(SUCCESS,msg,obj); } public static <T> Results<T> success(String resCode,String msg,T obj){ Results<T> result = new Results<T>(); result.setResCode(resCode); result.setMsg(msg); result.setObj(obj); return result; } public static <T> Results<T> failed() { return failed(ERROR,"失败",null); } public static <T> Results<T> failed(String msg) { return failed(ERROR,msg,null); } public static <T> Results<T> failed(String msg,T obj) { return failed(ERROR,msg,obj); } public static <T> Results<T> failed(String resCode,String msg) { return failed(resCode,msg,null); } public static <T> Results<T> failed(Integer resCode,String msg) { return failed(String.valueOf(resCode),msg); } public static <T> Results<T> failed(String resCode,String msg,T obj) { Results<T> result = new Results<T>(); result.setResCode(resCode); result.setMsg(msg); result.setObj(obj); return result; } public static <T> Results<T> failedNoPermission() { return failed(90005,"没有权限"); } public static <T> Results<T> failedNoPermission(String msg) { return failed(90005,msg); } public static <T> Results<T> failedParameterException() { return failed(90004,"参数异常"); } public static <T> Results<T> failedParameterException(String msg) { return failed(90004,msg); } public static <T> Results<T> failedLoginException() { return failed(90002,"登录失败"); } public static <T> Results<T> failedLoginException(String msg) { return failed(90002,msg); } public String getResCode() { return resCode; } public void setResCode(String resCode) { this.resCode = resCode; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getObj() { return obj; } public void setObj(T obj) { this.obj = obj; } @Override public String toString() { return "Results{" + "resCode='" + resCode + '\'' + ", msg='" + msg + '\'' + ", obj=" + obj + '}'; } }
在项目的根目录下面创建config
包,并在此包下面创建全局统一的返回结果对象ExceptionControllerAdvice
类
缺少参数抛出的异常是MissingServletRequestParameterException
单参数校验失败后抛出的异常是ConstraintViolationException
get请求的对象参数校验失败后抛出的异常是BindException
post请求的对象参数校验失败后抛出的异常是MethodArgumentNotValidException
不同异常对象的结构不同,对异常消息的提取方式也就不同。
ConstraintViolationException:单个参数校验失败(后端实际接收的一个字段)
BindException:表单对象参数违反约束,仅对于表单提交有效(接收参数没有加@RequestBody注解),对于以json格式提交将会失效
MethodArgumentNotValidException:JSON请求参数违反约束,为json格式有效(接收参数加上@RequestBody注解)
MissingServletRequestParameterException:参数缺失
MethodArgumentTypeMismatchException:请求参数的类型与处理器方法参数类型不匹配
HttpMessageNotReadableException:请求体为空、无效的JSON格式、无法将JSON转换为目标对象
@RestControllerAdvice public class ExceptionControllerAdvice { /** * BindException:表单对象参数违反约束,仅对于表单提交有效(接收参数没有加@RequestBody注解),对于以json格式提交将会失效 * MethodArgumentNotValidException:JSON请求参数违反约束,为json格式有效(接收参数加上@RequestBody注解) * ConstraintViolationException:单个参数校验失败(后端实际接收的一个字段) * @param e * @return */ @ResponseStatus(HttpStatus.OK) @ExceptionHandler(value = {BindException.class,ConstraintViolationException.class, MethodArgumentNotValidException.class}) public Results MethodArgumentNotValidExceptionHandler(Exception e) { // 从异常对象中拿到ObjectError对象 BindingResult br = null; if (e instanceof MethodArgumentNotValidException) { br = ((MethodArgumentNotValidException) e).getBindingResult(); } else if (e instanceof BindException) { br = ((BindException) e).getBindingResult(); } else if (e instanceof ConstraintViolationException) { Set<ConstraintViolation<?>> violations = ((ConstraintViolationException) e).getConstraintViolations(); if (CollectionUtils.isEmpty(violations)) { return Results.failed(String.valueOf(500)); } Map<String, String> map = violations.stream() .collect(Collectors.toMap(o -> { PathImpl x = (PathImpl) o.getPropertyPath(); return x.getLeafNode().toString(); }, ConstraintViolation::getMessage, (k1, k2) -> k1)); return Results.failed(400,map.toString()); } if (br.hasFieldErrors()) { List<FieldError> fieldErrorList = br.getFieldErrors(); List<String> errors = new ArrayList<>(fieldErrorList.size()); for (FieldError error : fieldErrorList) { errors.add(error.getField() + ":" + error.getDefaultMessage()); } // 然后提取错误提示信息进行返回 return Results.failed(400,errors.toString()); } // 然后提取错误提示信息进行返回 return Results.failed("校验错误"); } }
在项目的根目录下面创建model
包,并在此包下面创建全局统一的返回结果对象User
类
@Data public class User { @NotNull(message = "用户名不能为空") @Size(min = 5, max = 20, message = "用户名长度必须在5到20之间") private String username; @NotNull(message = "姓名不能为空") private String name; @NotNull(message = "年龄不能为空") @Min(value = 18, message = "年龄必须大于18") private Integer age; @Email(message = "邮箱格式不对") private String email; @NotEmpty(message = "爱好不能为空") private List<String> hobbies; }
在项目的根目录下面创建controller
包,并在此包下面创建全局统一的返回结果对象TestController
类
@Validated // 单/多个参数校验需要加的注解,不加参数校验不生效 @RestController public class TestController { // 会进入到MethodArgumentNotValidException异常处理方法 @PostMapping("/post") public Results post(@RequestBody @Validated User user) { try { return Results.success(user); } catch (Exception e) { return Results.failed(); } } // 多个参数校验需要在controller类上面添加@Validated注解(会进入到ConstraintViolationException全局异常方法里面) @GetMapping("/getConstraint") public Results getConstraint(@NotBlank(message = "姓名不能为空") String name, @NotNull(message = "年龄不能为空") @Min(value = 18, message = "年龄必须大于18") Integer age) { try { return Results.success(); } catch (Exception e) { return Results.failed(); } } // 会进入到BindException异常处理方法 @GetMapping("/getBindException") public Results getBindException(@Validated User user) { try { return Results.success(user); } catch (Exception e) { return Results.failed(); } } // 自定义BindException异常,会进入到BindException异常处理方法 @PostMapping("/testBindException") public Results testBindException(@RequestBody @Valid User user, BindingResult bindingResult) { try { // 检查验证结果 if (bindingResult.hasErrors()) { // 手动抛出BindException,触发异常处理 throw new BindException(bindingResult); } return Results.success(user); } catch (Exception e) { return Results.failed(); } } }
post方法
getConstraint方法
getBindException方法
准备工作:自定义两个分组。
提示: 继承Default并不是必须的。只是说,如果继承了Default,那么@Validated(value = Create.class)
的校验范畴就
为【Create】和【Default】;如果没继承Default,那么@Validated(value = Create.class)
的校验范畴只
为【Create】,而@Validated(value = {Create.class, Default.class})
的校验范畴才为【Create】和【Default】。
注: Default组和无参构造机制类似,当没有指定分组时,会默认当前校验属于Default组,但是一旦主动给当前校验指定
了分组(如上图中的name字段,主动指定了属于Create组),那么就不会再额外指定属于Default组了。
追注:当然,也可以画蛇添足的主动指定所属分组为Default。
@NotBlank(message = "密码不能为空",groups = {Add.class})
private String password;
@NotNull(message = "id不能为空",groups = {Update.class})
private Integer id;
public interface Update {
}
public interface Add extends Default {
}
@PostMapping("/addUser") public Results addUser(@Validated(User.Add.class) @RequestBody User uer) { try { return Results.success(); } catch (Exception e) { return Results.failed(); } } @PostMapping("/updateUser") public Results updateUser(@Validated(User.Update.class) @RequestBody User user) { try { return Results.success(); } catch (Exception e) { return Results.failed(); } }
addUser方法
updateUser方法
Address
类
@Data
public class Address {
@NotBlank(message = "城市不能为空",groups = {User.Add.class})
private String city;
@NotBlank(message = "城市编码不能为空",groups = {User.Add.class})
private String zipCode;
}
UserProfile
类
@Data
public class UserProfile {
@NotBlank(message = "个人简介不能为空",groups = {User.Add.class})
private String bio;
@NotBlank(message = "头像URL不能为空",groups = {User.Add.class})
private String avatarUrl;
@NotBlank(message = "社交媒体链接不能为空",groups = {User.Add.class})
private String linkedinUrl;
}
@Valid
private Address address;
@Valid
private UserProfile userProfile;
addUser方法
非常感谢以下博主:
https://blog.csdn.net/weixin_51262499/article/details/129910551
https://blog.csdn.net/csdnzhang365/article/details/129141189
https://blog.csdn.net/justry_deng/article/details/86571671
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。