一,為什么要做參數驗證?
永遠不要相信我們在后端接收到的數據,
1,防止別人通過接口亂刷服務:有些不懷好意的人或機構會亂刷我們的服務,例如:短信接口,
相信大家可能很多人在工作中遇到過這種情況
2,防止sql注入等行為:如果對數據會行嚴格的驗證,可以過濾掉大量的攻擊行為
3,防止客戶端出錯后的生成數據錯誤
所以,后端必須進行參數校驗,
即使前端已經校驗過,因為我們不能保證我們收到的請求都是由我們的前端程序發出
說明:劉宏締的架構森林是一個專注架構的博客,地址:https://www.cnblogs.com/architectforest
對應的源碼可以訪問這里獲取: https://github.com/liuhongdi/
說明:作者:劉宏締 郵箱: 371125307@qq.com
二,演示項目的相關信息
1,演示項目的地址:
https://github.com/liuhongdi/validator
2,演示項目的原理:
演示了三種情況:
直接針對controller的參數校驗
針對一個表單校驗
針對通用的參數用攔截器進行校驗
3,項目結構
如圖:
三, 如何使用validation庫?
1,pom.xml中引入validation
<!--validation begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!--validation end-->
2,validation有哪些現成的注解可用?
2.1 空檢查 @Null 驗證對象是否為空 @NotNull 驗證對象不為空 @NotBlank 驗證字符串不為空或不是空字符串 @NotEmpty 驗證對象不為Null,或者集合不為空
2.2 長度檢查 @Size 驗證對象長度,支持字符串,集合 @Length 驗證字符串大小(於 org.hibernate.validator.constraints 包中)
2.3 數值檢查 @Min 驗證數字是否大於等於指定值 @Max 驗證數字是否小於等於指定值 @Digits 驗證數字是否符合指定的格式 @Range 驗證數字是否在指定的范圍內 @Negative 驗證數字是否為負數 @NegativeOrZero 驗證數字是否小於等於0 @Positive 驗證數字是否為正數 @PositiveOrZero驗證數字是否大於等於0 @DecimalMin 驗證數字是否大於指定值 @DecimalMax 驗證數字是否小於等於指定值 2.4 時間檢查 @Future 檢查時間是否晚於現在 @FutureOrPresent 檢查時間是否非早於現在 @Past 檢查時間是否早於現在 @PastOrPresent 檢查時間是否非晚於現在
2.5 其他 @Email 檢查是否一個合法的郵箱地址 @Pattern 檢查是否符合指定的正則規則
3,如何配置使validator匹配到一個錯誤時立即返回,而不是等所有字段驗證完?
ValidatorConfig.java
@Configuration public class ValidatorConfig { /* *@author:liuhongdi *@date:2020/7/12 上午10:48 *@description:遇到第一個錯誤后立即返回,而不是遍歷完全部錯誤 */ @Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() .addProperty("hibernate.validator.fail_fast", "true") //快速驗證模式,有第一個參數不滿足條件直接返回 .buildValidatorFactory(); return validatorFactory.getValidator(); } @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); postProcessor.setValidator(validator()); return postProcessor; } }
四,例一:自定義一個validator
1,controller中直接用注解驗證參數:(適用於參數少的情況)
@Validated
@Controller
@RequestMapping("/home")
public class HomeController {
@GetMapping("/age") @ResponseBody public ResultUtil age(@Min(value = 10,message = "年齡最小為10")@Max(value = 100,message = "年齡最大為100") @RequestParam("age") Integer age) { return ResultUtil.success("this is age"); }
說明:使用以下url可以測試效果
http://127.0.0.1:8080/home/age http://127.0.0.1:8080/home/age?age=1 http://127.0.0.1:8080/home/age?age=60 http://127.0.0.1:8080/home/age?age=101
2, 自定義validator要用的注解
功能說明:傳遞的v參數必須是我們在常量中定義的值
VersionValid.java:用來定義注解
//驗證版本號是否符合系統定義 @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER,ElementType.FIELD}) @Constraint(validatedBy = VersionValidator.class) public @interface VersionValid { //用value傳遞的值 //String values(); //version無效時的提示內容 String message() default "version必須屬於預定義的值"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
VersionValidator.java (override這個方法:isValid,用來驗證數據是否合法)
public class VersionValidator implements ConstraintValidator<VersionValid,Object> { //預定義傳遞的值 //private String values; @Override public void initialize(VersionValid versionValidator) { //this.values = versionValidator.values(); } //version是否符合定義 @Override public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { // 切割獲取值 String[] value_array = Constants.APP_VERSION_LIST.split(","); Boolean isFlag = false; for (int i = 0; i < value_array.length; i++){ // 存在一致就跳出循環 if (value_array[i] .equals(value)){ isFlag = true; break; } } return isFlag; } }
在controller中調用VersionValid注解
@GetMapping("/home") @ResponseBody public ResultUtil home(@VersionValid(message = "v取值錯誤")@RequestParam("v") String version) { return ResultUtil.success("this is home"); }
3,測試效果:
錯誤
http://127.0.0.1:8080/home/home
http://127.0.0.1:8080/home/home?v=1
正確:
http://127.0.0.1:8080/home/home?v=1.0
五,例二:針對一個表單中多字段的validator
1,說明:有多個字段要驗證的表單,
如果寫到controller中會使代碼過於龐大而不便管理
通常我們會定義一個專門類進行處理
這里要說明的是:類中的驗證是針對每個字段的,
如果我們要比較類中兩個或以上的字段值,(例如:注冊頁面:驗證兩次輸入的密碼是否一致)
則需要針對整個類定義一個validator
2,定義一個注解:
PassValid.java
//用來驗證類中多個字段的validator的注解 @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PassValidator.class) @Documented public @interface PassValid { //報錯信息 String message() default "confirmPassword:兩次輸入密碼需一致"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; //密碼字段 String password(); //確認密碼字段 String password_confirm(); }
PassValidator.java 判斷兩次輸入的密碼是否一致的validator
//validator,判斷兩個密碼是否一致 public class PassValidator implements ConstraintValidator<PassValid, Object> { //密碼 private String passFieldName; //確認密碼 private String confirmFieldName; @Override public void initialize(final PassValid constraintAnnotation) { passFieldName = constraintAnnotation.password(); confirmFieldName = constraintAnnotation.password_confirm(); } @Override public boolean isValid(final Object src, final ConstraintValidatorContext context) { BeanWrapperImpl wrapper = new BeanWrapperImpl(src); Object passObj = wrapper.getPropertyValue(passFieldName); Object confirmObj = wrapper.getPropertyValue(confirmFieldName); return passObj != null && passObj.equals(confirmObj); } }
應用上面創建的validator,實現針對整個表單的驗證
@PassValid(password = "password", password_confirm = "confirmPassword") public class UserVo { @Size(min = 2,max = 10,message = "name:姓名長度必須為1到10") private String name; public String getName() { return this.name; } public void setName(String name) { this.name = name; } //@Range(min=10, max=100,message = "年齡需位於10到100之間") @Min(value = 10,message = "age:年齡最小為10") @Max(value = 100,message = "age:年齡最大為100") private int age; public int getAge() { return this.age; } public void setAge(int age) { this.age = age; } @NotNull(message = "mobile:手機號碼不能為空") @Size(min = 11, max = 11, message = "mobile:手機號碼必須為11位") @Pattern(regexp="^[1]\\d{10}$", message="mobile:手機號碼格式錯誤") private String mobile; public String getMobile() { return this.mobile; } public void setMobile(String mobile) { this.mobile = mobile; } @NotBlank(message = "email:郵箱不能為空") @Email(message = "email:郵箱格式錯誤") private String email; public String getEmail() { return this.email; } public void setEmail(String email) { this.email = email; } @NotBlank(message = "password:密碼不能為空") String password; public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } @NotBlank(message = "confirmPassword:確認密碼不能為空") String confirmPassword; public String getConfirmPassword() { return this.confirmPassword; } public void setConfirmPassword(String confirmPassword) { this.confirmPassword = confirmPassword; } // @Pattern(regexp="^[0-9]{4}-[0-9]{2}-[0-9]{2}$",message="出生日期格式不正確") //@Pattern(regexp="^(\\d{18,18}|\\d{15,15}|(\\d{17,17}[x|X]))$", message="身份證格式錯誤") }
在controller中應用:
@GetMapping("/user") //@ResponseBody public String user() { return "user/user"; } @PostMapping("/usersaveed") @ResponseBody //public ResultUtil usersaveed(@Validated UserVo userVo) { public ResultUtil usersaveed(@Validated UserVo userVo) { System.out.println("----------email:"+userVo.getEmail()); return ResultUtil.success("this is in usersaveed"); }
3,測試效果:
http://127.0.0.1:8080/home/user
如圖:
六,例三:針對通用的參數,用interceptor做校驗
1,說明:
我們在訪問接口時,通常有一些通用的參數要傳遞,
例如:
appid:通常是所在平台
version:客戶端的版本
uuid:客戶端的唯一id
2,定義interceptor要匹配的地址
DefaultMvcConfig.java
@Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class DefaultMvcConfig implements WebMvcConfigurer { @Resource private ValidatorInterceptor validatorInterceptor; /** * 添加Interceptor * 檢驗參數不能全部覆蓋,因為可能有供第三方訪問的接口地址,例如支付的回調接口 * 所以需要把不用的排除掉 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(validatorInterceptor) .addPathPatterns("/**") //所有請求都需要進行報文簽名sign .excludePathPatterns("/home/age**","/home/home**","/home/user**","/js/**","/"); //排除age/home...url } }
ValidatorInterceptor.java :實現通用參數驗證的interceptor
@Component public class ValidatorInterceptor implements HandlerInterceptor { /* *@author:liuhongdi *@date:2020/7/1 下午4:00 *@description:檢查通用的變量是否存在,是否合法 * @param request:請求對象 * @param response:響應對象 * @param handler:處理對象:controller中的信息 * * *@return:true表示正常,false表示被攔截 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //檢查appid是否存在? String appId = request.getParameter("appid"); if (appId == null) { throw new BusinessException(ResponseCode.ARG_NO_APPID); } //appid是否符合定義 if (!Constants.APP_ID_LIST.contains(appId)) { throw new BusinessException(ResponseCode.ARG_APPID_VALID); } //version參數是否存在 String version = request.getParameter("version"); if (version == null) { throw new BusinessException(ResponseCode.ARG_NO_VERSION); } //當appid是ios時,version是否符合定義 if (appId.equals("IOS")) { if (!Constants.IOS_VERSION_LIST.contains(version)) { throw new BusinessException(ResponseCode.ARG_VERSION_VALID); } } //uuid參數是否存在 String uuid = request.getParameter("uuid"); if (uuid == null) { throw new BusinessException(ResponseCode.ARG_NO_UUID); } //sign校驗無問題,放行 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
在controller中調用:
@GetMapping("/category") @ResponseBody public ResultUtil category(@Pattern(regexp="^[a-zA-Z]{4}$", message="分類取值錯誤")@RequestParam("cate") String category) { return ResultUtil.success("this is in category"); }
3,測試效果:
錯誤的訪問:
http://127.0.0.1:8080/home/category
正確的訪問:
http://127.0.0.1:8080/home/category?appid=IOS&version=1.1&uuid=06C58F98-51F7-4C35-AC4C-B56D265CD3E9&cate=abcd
七,查看spring boot的版本
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE)