在实际开发中,前端校验并不安全,任何人都可以通过接口来调用我们的服务,就算加了一层token的校验,有心人总会转空子,来传各式各样错误的参数,如果后端不校验,导致数据库数据混乱、特别是关于金额的数据,可能一个接口把公司都给干倒了
/** * 用于返回 * @param*/ @ApiModel("统一返回类") public class Results { 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 Results success(){ return success(SUCCESS,"成功",null); } public static Results success(String msg){ return success(SUCCESS,msg,null); } public static Results success(T obj){ return success(SUCCESS,"成功",obj); } public static Results success(String msg,T obj){ return success(SUCCESS,msg,obj); } public static Results success(String resCode,String msg,T obj){ Results result = new Results (); result.setResCode(resCode); result.setMsg(msg); result.setObj(obj); return result; } public static Results failed() { return failed(ERROR,"失败",null); } public static Results failed(String msg) { return failed(ERROR,msg,null); } public static Results failed(String msg,T obj) { return failed(ERROR,msg,obj); } public static Results failed(String resCode,String msg) { return failed(resCode,msg,null); } public static Results failed(Integer resCode,String msg) { return failed(String.valueOf(resCode),msg); } public static Results failed(String resCode,String msg,T obj) { Results result = new Results (); result.setResCode(resCode); result.setMsg(msg); result.setObj(obj); return result; } public static Results failedNoPermission() { return failed(90005,"没有权限"); } public static Results failedNoPermission(String msg) { return failed(90005,msg); } public static Results failedParameterException() { return failed(90004,"参数异常"); } public static Results failedParameterException(String msg) { return failed(90004,msg); } public static Results failedLoginException() { return failed(90002,"登录失败"); } public static Results 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 + '}'; } }
@ApiModel("测试 validation 入参") @Data public class TestDto { @ApiModelProperty(value = "名字",required = true) private String name; @ApiModelProperty(value = "年龄",required = true) private Integer age; @ApiModelProperty(value = "爱好",required = true) private Listhobbies; }
我们可以看见如果参数过大,要一个一个筛选条件十分浪费时间
@RestController // lombok 的日志注解 @Slf4j // swagger 的注解 @Api("测试") public class TestController { @PostMapping("/testValidation") // swagger 的注解 @ApiOperation("测试 validation") public Results testValidation(@RequestBody TestDto dto){ try { log.info("test 入参 dto={}",dto); // 这要一个一个的塞,很浪费时间 if (dto.getName() == null || "".equals(dto.getName().trim())){ return Results.failed("名字不能为空"); } if (dto.getAge() == null){ return Results.failed("年龄不能为空"); } if (dto.getHobbies() == null || dto.getHobbies().size() == 0){ return Results.failed("爱好不能为空"); } return Results.success(); } catch (Exception e) { log.error("test 报错",e); return Results.failed(); } } }
javax.validation validation-api 2.0.1.Final org.springframework.boot spring-boot-starter-validation
@RestControllerAdvice public class ExceptionControllerAdvice { @ResponseStatus(HttpStatus.OK) @ExceptionHandler(MethodArgumentNotValidException.class) public Results MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { // 从异常对象中拿到ObjectError对象 BindingResult br = e.getBindingResult(); if (br.hasFieldErrors()) { ListfieldErrorList = br.getFieldErrors(); List errors = new ArrayList<>(fieldErrorList.size()); for (FieldError error : fieldErrorList) { errors.add(error.getField() + ":" + error.getDefaultMessage()); } // 然后提取错误提示信息进行返回 return Results.failed(errors.toString()); } // 然后提取错误提示信息进行返回 return Results.failed("校验错误"); } }
@ApiModel("测试 validation 入参") @Data public class TestDto { @ApiModelProperty(value = "名字",required = true) // 适用于 String 类型的数据上,加了@NotBlank 注解的参数不能为 Null 且 trim() 之后 size > 0,必须有实际字符 @NotBlank(message = "名字不能为空") private String name; @ApiModelProperty(value = "年龄",required = true) @NotNull(message = "年龄不能为空") // 适用于基本数据类型(Integer,Long,Double等等),当 @NotNull 注解被使用在 String 类型的数据上,则表示该数据不能为 Null(但是可以为 Empty) private Integer age; @ApiModelProperty(value = "爱好",required = true) // 适用于 String、Collection集合、Map、数组等等,加了@NotEmpty 注解的参数不能为 Null 或者 长度为 0 @NotEmpty(message = "年龄不能为空") private Listhobbies; }
必须要加上 @Valid 或者 @Validated,后续我会讲解这两个有什么不同,目前来说,都可以用,但推荐用 @Validated
@RestController @Slf4j @Api("测试") public class TestController { @PostMapping("/testValidation") @ApiOperation("测试 validation") // 必须要加上 @Valid 或者 @Validated public Results testValidation( // 必须要加上 @Valid 或者 @Validated @Valid @RequestBody TestDto dto){ try { log.info("test 入参 dto={}",dto); return Results.success(); } catch (Exception e) { log.error("test 报错",e); return Results.failed(); } } }
这个颜色的是常用的
注解 | 含义 |
---|---|
@Null | 任何类型 必须为null |
@NotBlank | 字符串、字符 字符类不能为null,且去掉空格之后长度大于 |
@NotNull | 任何类型 不能为null |
@Length(min = 6, max = 8, message = “密码长度为6-8位。”) | 字符串的大小必须在指定的范围内 |
@NotEmpty | 适用于 String、Collection集合、Map、数组等等,加了@NotEmpty 注解的参数不能为 Null 或者 长度为 0 |
@AssertTrue | Boolean、boolean 布尔属性必须是true |
@AssertFalse | Boolean、boolean 布尔属性必须是false |
@Min(10) | 必须是一个数字,其值必须大于等于指定的最小值(我这填的是10)(整型) |
@Max(10) | 必须是一个数字,其值必须小于等于指定的最大值(我这填的是10)(整型) |
@DecimalMin(“10”) | 必须是一个数字,其值必须大于等于指定的最小值(我这填的是10)(字符串,可以是小数) |
@DecimalMax(“10”) | 必须是一个数字,其值必须小于等于指定的最大值(我这填的是10)(字符串,可以是小数) |
@Size(max = 10,min = 1) | 集合 限定集合大小 |
@Digits(integer = 3, fraction = 2, message = “请输入有效的数字”) private double number; | @Digits 用于验证数字的整数位数和小数位数。该注解的 integer 和 fraction 属性分别用于指定整数位数和小数位数的限制。 integer 属性用于指定数字的最大整数位数。它是一个整数值,表示数字允许的最大整数位数。例如,integer = 3 表示数字最多可以有三位整数部分。 fraction 属性用于指定数字的最大小数位数。它是一个整数值,表示数字允许的最大小数位数。例如,fraction = 2 表示数字最多可以有两位小数部分。 |
@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校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性 |
@ScriptAssert(lang=, script=, alias=) | 要有Java Scripting API 即JSR 223 (“Scripting for the JavaTM Platform”)的实现 |
@SafeHtml(whitelistType=, additionalTags=) | classpath中要有jsoup包 |
Spring Validation验证框架对参数的验证机制提供了@Validated(Spring’s JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。
@Valid属于javax.validation包下,是jdk给提供的 是使用Hibernate validation的时候使用
@Validated是org.springframework.validation.annotation包下的,是spring提供的 是只用Spring Validator校验机制使用
说明:java的JSR303声明了@Valid这类接口,而Hibernate-validator对其进行了实现
@Validation对@Valid进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同,这里主要就这几种情况进行说明。
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
- 分组
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。
- 注解地方
@Validated:用在类型、方法和方法参数上。但不能用于成员属性(field)
@Valid:可以用在方法、构造函数、方法参数和成员属性(field)上 所以可以用@Valid实现嵌套验证
总结:
@Valid 和 @Validated 两者都可以对数据进行校验,待校验字段上打的规则注解(@NotNull, @NotEmpty等)都可以对 @Valid 和 @Validated 生效;
@Valid 进行校验的时候,需要用 BindingResult 来做一个校验结果接收。当校验不通过的时候,如果手动不 return ,则并不会阻止程序的执行;
@Validated 进行校验的时候,当校验不通过的时候,程序会抛出400异常,阻止方法中的代码执行,这时需要再写一个全局校验异常捕获处理类,然后返回校验提示。
总体来说,@Validated 使用起来要比 @Valid 方便一些,它可以帮我们节省一定的代码,并且使得方法看上去更加的简洁。
在开发中,新增、修改两个接口,一般关系就在于新增时ID可以为空,修改时ID不能为空,那我们如果要使用 validation 用于参数校验,创建两个实体类就非常的不划算,这时
import javax.validation.groups.Default; public interface Update extends Default { }
@ApiModel("测试 validation 入参") @Data public class TestDto { @ApiModelProperty(value = "ID",required = true) // 新增时ID为空,修改时ID不能为空 @NotNull(message = "ID不能为空",groups = Update.class) private Integer id; @ApiModelProperty(value = "名字",required = true) @NotBlank(message = "名字不能为空") private String name; }
@RestController @Slf4j @Api("测试") public class TestController { @PostMapping("/testAdd") @ApiOperation("测试 新增") public Results testAdd(@Validated @RequestBody TestDto dto){ try { log.info("testAdd 入参 dto={}",dto); return Results.success(); } catch (Exception e) { log.error("testAdd 报错",e); return Results.failed(); } } @PostMapping("/testUpdate") @ApiOperation("测试 新增") public Results testUpdate(@Validated(Update.class) @RequestBody TestDto dto){ try { log.info("testUpdate 入参 dto={}",dto); return Results.success(); } catch (Exception e) { log.error("testUpdate 报错",e); return Results.failed(); } } }