總結篇-後臺引數驗證的幾種方式
1.前言
引數驗證是一個常見的問題,無論是前端還是後臺,都需對使用者輸入進行驗證,以此來保證系統資料的正確性。對於web來說,有些人可能理所當然的想在前端驗證就行了,但這樣是非常錯誤的做法,前端程式碼對於使用者來說是透明的,稍微有點技術的人就可以繞過這個驗證,直接提交資料到後臺。無論是前端網頁提交的介面,還是提供給外部的介面,引數驗證隨處可見,也是必不可少的。前端做驗證只是為了使用者體驗,比如控制按鈕的顯示隱藏,單頁應用的路由跳轉等等。後端才是最終的保障。總之,一切使用者的輸入都是不可信的。
2.常見的驗證的方式
前端的校驗是必須的,這個很簡單,因為客戶體驗。後臺的校驗更是必須的,關鍵在於如何與目前我們的分層思想(控制層、業務層、持久層)綜合起來考慮。在每層都要進行校驗嗎?還是隻在是某個特定層做就可以了?是否有好的校驗框架(如前端的jquery校驗框架、springmvc校驗框架)?總之校驗框架還是有很多的,原理不就是對後端接收的資料進行特定規則的判斷,那我們怎麼制定規則,有怎麼去檢驗呢?
1、表現層驗證:SpringMVC提供對JSR-303的表現層驗證;
2、業務邏輯層驗證:Spring3.1提供對業務邏輯層的方法驗證(當然方法驗證可以出現在其他層,但筆者覺得方法驗證應該驗證業務邏輯);
3、DAO層驗證:Hibernate提供DAO層的模型資料的驗證(可參考hibernate validator參考文件的7.3. ORM整合)。
4、資料庫端的驗證:通過資料庫約束來進行;
5、客戶端驗證支援:JSR-303也提供程式設計式驗證支援。
1)通過 if-if 判斷
if(string.IsNullOrEmpty(info.UserName))
{
return FailJson("使用者名稱不能為空");
}
逐個對引數進行驗證,這種方式最粗暴.如果引數一多,就要寫n多的if-if,相當繁瑣,更重要的是這部分判斷沒法重用,另一個方法又是這樣判斷.。
2) 自定義註解實現引數校驗
切面攔截controller方法,然後捕獲帶@CheckParam註解方法引數例項,最後反射例項校驗。
controller:
@RequestMapping(value = "update" )
@ResponseBody
public ResultBean update(@CheckParam User user){
return ResultBean.ok();
}
model:
public class User implements Serializable{
@CheckParam(notNull = true)
private String username;
}
annotation:
@Target(value={ElementType.PARAMETER,ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckParam {
boolean notNull() default false;
}
aspect:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@Component
@Aspect
public class CheckParamAspect {
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void methodPointCut() {}
/**
* 環繞切入方法
**/
@Around("methodPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature msig = (MethodSignature) point.getSignature();
Method method = msig.getMethod();
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
Object[] args = point.getArgs();
for (int i = 0; i < args.length; i++) {
Object obj = args[i];
MethodParameter mp = new MethodParameter(method,i);
mp.initParameterNameDiscovery(u);
GenericTypeResolver.resolveParameterType(mp, method.getClass());//Spring處理引數
//String paramName = mp.getParameterName();//引數名
CheckParam anno = mp.getParameterAnnotation(CheckParam.class);//引數註解
if(anno != null){
check(obj);
}
}
return point.proceed();
}
/**
* 校驗成員變數
**/
private void check(Object obj) throws IllegalAccessException {
Class clazz = obj.getClass();
for(Field field : clazz.getDeclaredFields()){
CheckParam cp = field.getAnnotation(CheckParam.class);
if(cp != null){
check(obj,clazz, field,cp);
}
}
}
/**
* 取出註解,校驗變數
**/
private void check(Object obj, Class clazz, Field field, CheckParam cp) throws IllegalAccessException {
if(cp.notNull()){
field.setAccessible(true);
Object f = field.get(obj);
if(StringUtils.isEmpty(f)){
throw new IllegalArgumentException("類" + clazz.getName() + "成員" + field.getName() + "檢測到非法引數");
}
}
}
}
3.自定義ValidationUtils
表單驗證工具類ValidationUtils,依賴包commons-lang
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
public class ValidateUtils {
/**
* @param fields
* @param params
* @return
* 不存在的校驗規則:返回true
* 關鍵字不按要求寫:返回true
*/
public static SKResult validate(ValidField[] fields, Map<String, String> params){
try {
for(ValidField field : fields){
String name = field.getName();
String desc = field.getDes();
boolean isValid = field.isValid();
String[] rules = field.getRules();
String value = params.get(name); // 對應請求引數值
if(!isValid){
return new SKResult(true, "");
}
for(String rule : rules){
String[] arr = rule.replaceAll(" ", "").split(":");
String arr1 = arr[0]; // required
String arr2 = arr[1]; // true
switch (arr1) {
case "required": // 必須項 required:true|false
if(Boolean.parseBoolean(arr2)){
if(value==null || value.trim().length()==0){
return new SKResult(false, desc+"不能為空");
}
}
break;
case "number": // 必須輸入合法的數字(負數,小數) number:true|false
if(Boolean.parseBoolean(arr2)){
try{
Double.valueOf(value);
}catch(Exception e){
return new SKResult(false, desc+"數值型別不合法");
}
}
break;
default:
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("===ValidField格式不合法,請注意檢查!");
return new SKResult(true, "ValidField格式不合法");
}
return new SKResult(true, "校驗通過");
}
public static void main(String[] args) {
Map<String, String> params = new HashMap<String, String>();
params.put("username", "18702764599");
params.put("password", "123");
ValidField[] fields = {
new ValidField("username", "手機號", true, new String[]{
"required:true",
"isTel:true"
"min:5"
"max:5"
}),
new ValidField("password", "密碼", true, new String[]{
"required:true",
"isPassword:true",
"equalTo:#username"
"max:2"
})
};
SKResult sk = ValidateUtils.validate(fields, params);
System.out.println(sk);
//SKResult [result=true, respMsg=校驗通過, obj=null, type=null]
}
}
SKResult :
public class SKResult {
// 返回程式碼
private boolean result;
// 錯誤資訊
private String respMsg;
private Object obj;
//set.get方法
@Override
public String toString() {
return "SKResult [result=" + result + ", respMsg=" + respMsg + ", obj="
+ obj + ", type=" + type + "]";
}
}
ValidField :
public class ValidField {
/**
* 欄位名
*/
private String name;
/**
* 欄位描述
*/
private String des;
/**
* 為true必須校驗
*/
private boolean isValid = false;
/**
* 校驗規則
*/
private String[] rules;
public String[] getRules() {
return rules;
}
public void setRules(String[] rules) {
this.rules = rules;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDes() {
return des;
}
public void setDes(String des) {
this.des = des;
}
public boolean isValid() {
return isValid;
}
public void setValid(boolean isValid) {
this.isValid = isValid;
}
public ValidField(String name, String des, boolean isValid, String[] rules) {
super();
this.name = name;
this.des = des;
this.isValid = isValid;
this.rules = rules;
}
}
4) JSR-303規範,Bean Validation
JSR 303(Java Specification Requests 規範提案)是JAVA EE 6中的一項子規範,一套JavaBean引數校驗的標準,叫做Bean Validation。JSR 303用於對Java Bean中的欄位的值進行驗證,Spring MVC 3.x之中也大力支援 JSR-303,可以在控制器中對錶單提交的資料方便地驗證。
<!--jsr 303-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<!-- hibernate validator-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.0.Final</version>
</dependency>
package com.example.demo;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.ValidationException;
import javax.validation.Validator;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.util.Iterator;
import java.util.Set;
/**
* @author lanxinghua
* @date 2018/08/05 15:51
* @description
*/
public class ValidateTestClass {
@NotNull(message = "reason資訊不可以為空")
@Pattern(regexp = "[1-7]{1}", message = "reason的型別值為1-7中的一個型別")
private String reason;
public void setReason(String reason) {
this.reason = reason;
}
public void validateParams() {
//呼叫JSR303驗證工具,校驗引數
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<ValidateTestClass>> violations = validator.validate(this);
Iterator<ConstraintViolation<ValidateTestClass>> iter = violations.iterator();
if (iter.hasNext()) {
String errMessage = iter.next().getMessage();
throw new ValidationException(errMessage);
}
}
}
package com.example.demo;
/**
* @author lanxinghua
* @date 2018/08/05 15:56
* @description
*/
public class ValidateTestClassValidateTest {
public static void main(String[] args) {
ValidateTestClass validateTestClass = new ValidateTestClass();
validateTestClass .setReason(null);
validateTestClass .validateParams(); //呼叫驗證的方法
}
}
5. JSR-303規範,Bean Validation在SSM專案中使用
JSR和Hibernate validator的校驗只能對Object的屬性進行校驗。
5.1 Model 中新增校驗註解
public class Book {
private long id;
@NotEmpty(message = "書名不能為空")
private String bookName;
@NotNull(message = "ISBN號不能為空")
private String bookIsbn;
@DecimalMin(value = "0.1",message = "單價最低為0.1")
private doubleprice; // getter setter ....... }
5.2 在controller中使用此校驗
@RequestMapping(value = "/book",method = RequestMethod.POST)
public void addBook(@RequestBody @Valid Book book) {
System.out.println(book.toString());
}
5.3 分組驗證
對同一個Model,我們在增加和修改時對引數的校驗也是不一樣的,這個時候我們就需要定義分組驗證,步驟如下:
定義兩個空介面,分別代表Person物件的增加校驗規則和修改校驗規則
//可以在一個Model上面新增多套引數驗證規則,此介面定義新增Person模型修改時的引數校驗規則
public interface PersonAddView {}
public interface PersonModifyView {}
Model上添加註解時使用指明所述的分組
public class Person {
private long id;
/**
* 新增groups 屬性,說明只在特定的驗證規則裡面起作用,不加則表示在使用Deafault規則時起作用
*/
@NotNull(groups = {PersonAddView.class, PersonModifyView.class}, message= "新增、修改使用者時名字不能為空",payload = ValidateErrorLevel.Info.class)
@ListNotHasNull.List({
@ListNotHasNull(groups = {PersonAddView.class}, message = "新增上Name不能為空"),
@ListNotHasNull(groups = {PersonModifyView.class}, message = "修改時Name不能為空")})
private String name;
@NotNull(groups = {PersonAddView.class}, message = "新增使用者時地址不能為空")
private String address;
@Min(value = 18, groups = {PersonAddView.class}, message = "姓名不能低於18歲")
@Max(value = 30, groups = {PersonModifyView.class}, message = "姓名不能超過30歲")
private int age;
//getter setter 方法......
}
此時啟用校驗和之前的不同,需要指明啟用哪一組規則
/**
* 備註:此處@Validated(PersonAddView.class)表示使用PersonAndView這套校驗規則,若使用@Valid 則表示使用預設校驗規則,若兩個規則同時加上去,則只有第一套起作用
* 修改Person物件
* 此處啟用PersonModifyView這個驗證規則
*/
@RequestMapping(value = "/person", method = RequestMethod.PUT)
public void modifyPerson(@RequestBody @Validated(value ={PersonModifyView.class}) Person person) {
System.out.println(person.toString());
}
6. Spring validator 方法級別的校驗
JSR和Hibernate validator的校驗只能對Object的屬性進行校驗,不能對單個的引數進行校驗,spring 在此基礎上進行了擴充套件,添加了MethodValidationPostProcessor攔截器,可以實現對方法引數的校驗
public @NotNull UserModel get2(@NotNull @Size(min = 1) Integer uuid) {
//獲取 User Model
UserModel user = new UserModel(); //此處應該從資料庫獲取
return user;
}
7. java開源驗證框架OVAL
我發現我們公司dubbo服務暴露的介面用這套框架來驗證。
//下單支付預處理
@Validator({@Check(name = "orderDTO", adapter = NotNull.class, message = "訂單詳情不能為空", errorCode = "10")})
Result<BossOrderDTO> orderPayPrepare(BossOrderDTO orderDTO);
hibernater-validator依賴於validation-api,說明這個框架是實現了bean validation規範的,從測試中也可以看出,既可以使用javax.validation包下的註解來做校驗,也可以使用自身的註解;而oval不依賴於validation-api.兩者大同小異,實現的原理也差不多. Java開源驗證框架Oval是一個可擴充套件的Java物件資料驗證框架,功能強大使用簡單,驗證規則可通過配置檔案、註解等方式進行設定,規則的編寫可以使用純Java、JavaScript 、Groovy 、BeanShell等語言。
<dependency>
<groupId>net.sf.oval</groupId>
<artifactId>oval</artifactId>
<version>1.81</version>
</dependency>
實現Oval實體物件類,使用者的年齡和名字進行校驗,具體程式碼如下:
public class OvalTest {
@Min(18)
private int age;
@Length(min = 6, max = 12)
private String name;
public static void main(String[] args) {
OvalTest ovalTest = new OvalTest();
ovalTest.age = 12;
ovalTest.name = "yoodb";
Validator validator = new Validator();
List<ConstraintViolation> ret = validator.validate(ovalTest);
System.out.println(ret);
}
}
JSR提供的校驗註解:
@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=) 被註釋的元素必須在合適的範圍內