请求规范化
一个接口一般对参数(请求数据)都会进行安全校验,参数校验的重要性自然不必多说,那么如何对参数进行校验就有讲究了。
以前的业务层校验
首先我们来看一下最常见的做法,就是在业务层进行参数校验:
public String addUser(User user) {
if (user == null || user.getId() == null || user.getAccount() == null || user.getPassword() == null || user.getEmail() == null) {
return "对象或者对象字段不能为空";
}
if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
return "不能输入空字符串";
}
if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
return "账号长度必须是6-11个字符";
}
if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
return "密码长度必须是6-16个字符";
}
if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
return "邮箱格式不正确";
}
// 参数校验完毕后这里就写上业务逻辑
return "success";
}
这样做当然是没有什么错的,而且格式排版整齐也一目了然,不过这样太繁琐了,这还没有进行业务操作呢光是一个参数校验就已经这么多行代码,实在不够优雅。
现在参数校验的几种情况
PathVariable 校验
在定义 Restful 风格的接口时,通常会采用 @PathVariable
指定关键业务参数,如下:
@GetMapping("/path/{group:[a-zA-Z0-9_]+}/{userid}")
@RestController
public String path(@PathVariable("group") String group, @PathVariable("userid") Integer userid) {
return group + ":" + userid;
}
{group:[a-zA-Z0-9_]+} 这样的表达式指定了 group 必须是以大小写字母、数字或下划线组成的字符串。 类似这样可以使用正则去匹配
这种情况如果未匹配上就会返回404错误
方法参数校验
大多数情况下,我们都会直接将HTTP请求参数映射到方法参数上。
@GetMapping("/param")
@RestController
public String param(@RequestParam("group")@Email String group,
@RequestParam("userid") Integer userid) {
return group + ":" + userid;
}
@RequestParam 声明了映射,此外我们还为 group 定义了一个规则(Email格式)
@Email 这些注解哪里来的呢 ?
JSR-303
JSR-303是Java为Bean数据合法性校验提供的标准框架,它定义了一套可标注在成员变量,属性方法上的校验注解。注解是JSR-303 定义的 而他的实现就是通过 Hibernate Validator 来提供的,且已经包含在spring boot web stater
场景下了,若不是spring boot 框架则需引入
<!-- Hibernate Validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.7.Final</version>
</dependency>
下面是常用的注解
JSR-303定义的注解
@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=) 被注释的元素必须在合适的范围内
使用方法参数校验
将注解标在controller
参数上
@GetMapping("/param")
public String param(@RequestParam("model")@Size(min=4, max=4,message = "请检查型号的长度是否有问题") String model) {
return model;
}
开启方法参数的校验
将其注入到ioc
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
并将**@Validated ** 标注在controller类上
@RestController
@RequestMapping("/validate")
@Validated
public class ValidateController { // ....
}
表单对象校验
页面的表单通常比较复杂,此时可以将请求参数封装到表单对象中,
当使用方法参数校验
对表单进行校验时,要在方法参数部分写好多 , 这时就需要对象校验了
这时仍然适用JSR-303 的校验规则
将JSR-303注解标在对象上 使用**@Validated **标注在参数上 进行校验
public class Laptop {
@NotNull
@Size(min=4, max=4,message = "请检查型号的长度是否有问题")
@JsonProperty("型号")
private String model;
@NotNull
@JsonProperty("速度G")
private Double speed;
//........
}
分组校验
将这些注解注释在实体类上在controller层需要使用@Validated
/@Valid
对注解的校验
@Validated注解表示使用Spring的校验机制,支持分组校验,声明在入参上.
Validated 可以分组校验:意思是有些情况我们不需要对某些参数进行检验,这个时候就可以使用分组校验 ,下面是步骤
-
定义分组接口做标识(仅仅是标识)
public interface GroupA { } public interface GroupB { }
-
使用校验注解标注在属性上并进行分组 不标识默认会全匹配
public class Laptop { @NotNull @Size(max=4,message = "请检查型号的长度是否有问题",groups={GroupA.class}) @JsonProperty("型号") private String model; //........ }
-
@Validated 注解校验时指定组
@Validated(value= {GroupB.class}) Laptop laptop
@Valid注解表示使用Hibernate的校验机制,不支持分组校验,声明在入参上.
在controller参数后面要紧跟BindingResult对象,该对象用于获取当校验失败时的异常信息.
根据BindingResult 的异常信息处理
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated User user, BindingResult bindingResult) {
// 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
for (ObjectError error : bindingResult.getAllErrors()) {
String error = error.getDefaultMessage();
//处理异常结果
}
return "";
}
自定义校验注解
有时候,第三方库中并没有我们想要的校验类型,好在系统提供了很好的扩展能力,我们可以自定义检验。
比如检验手机号
步骤
-
编写校验注解
// 我们可以直接拷贝系统内的注解如@Min,复制到我们新的注解中,然后根据需要修改。 @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented //注解的实现类。 @Constraint(validatedBy = {IsMobileValidator.class 实现类}) public @interface IsMobile { //校验错误的默认信息 String message() default "手机号码格式有问题"; //是否强制校验 boolean isRequired() default false; //组校验 Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
-
编写实现类
// 自定义注解一定要实现ConstraintValidator接口,里面的两个参数 // 第一个为 具体要校验的注解 // 第二个为 校验的参数类型 public class IsMobileValidator implements ConstraintValidator<IsMobile, String> { private boolean required = false; private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}"); //工具方法,判断是否是手机号 public static boolean isMobile(String src) { if (StringUtils.isEmpty(src)) { return false; } Matcher m = mobile_pattern.matcher(src); return m.matches(); } @Override public void initialize(IsMobile constraintAnnotation) { required = constraintAnnotation.isRequired(); } @Override public boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) { //是否为手机号的实现 if (required) { return isMobile(phone); } else { if (StringUtils.isEmpty(phone)) { return true; } else { return isMobile(phone); } } } }
-
测试
标注在属性上测试即可
参数校验异常处理
参数异常处理可以在controller参数后面要紧跟BindingResult对象,该对象用于获取当校验失败时的异常信息.
BindingResult 处理异常
根据BindingResult 的异常信息处理
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated User user, BindingResult bindingResult) {
// 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
for (ObjectError error : bindingResult.getAllErrors()) {
String error = error.getDefaultMessage();
//处理异常结果
}
return "";
}
但是这样就会重复写很多异常处理,尽管可以做成统一接口 ,但还是麻烦.
全局异常处理
参数校验失败会自动引发异常,那正好使用SpringBoot全局异常处理来达到一劳永逸的效果
在包 solveexception
中新建类,在这个类上加上@ControllerAdvice或@RestControllerAdvice注解,这个类就配置成全局处理类了。(这个根据你的Controller层用的是@Controller还是@RestController来决定,返回页面还是数据)
再类中新增方法 加上@ExceptionHandler
添加要处理的类 ,编写对应逻辑就完成了相关异常处理
例如
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取错误提示信息进行返回
//推荐使用统一返回类型
return objectError.getDefaultMessage();
}
}
这个时候我们就可以编写自定义异常然后再全局进行处理