SpringBoot參數校驗

本篇概述

  在正常的項目開發中,我們常常需要對程序的參數進行校驗來保證程序的安全性。參數校驗非常簡單,說白了就是對參數進行正確性驗證,例如非空驗證、範圍驗證、類型驗證等等。校驗的方式也有很多種。如果架構設計的比較好的話,可能我們都不需要做任何驗證,或者寫比較少的代碼就可以滿足驗證的需求。如果架構設計的有缺陷,或者說壓根就沒有架構的話,那麼我們對參數進行驗證時,就需要我們寫大量相對重複的代碼進行驗證了。


手動參數校驗

  下面我們還是以上一篇的內容爲例,我們首先手動對參數進行校驗。下面爲Controller源碼:

package com.jilinwula.springboot.helloworld.controller;

import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;
import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;
import com.jilinwula.springboot.helloworld.query.UserInfoQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/userinfo")
public class UserInfoController {

    @Autowired
    private UserInfoRepository userInfoRepository;

    @GetMapping("/query")
    public Object list(UserInfoQuery userInfo) {
        if (StringUtils.isEmpty(userInfo.getUsername())) {
            return "賬號不能爲空";
        }
        if (StringUtils.isEmpty(userInfo.getRoleId()) || userInfo.getRoleId() > 100 || userInfo.getRoleId() < 1) {
            return "權限不能爲空,並且範圍爲[1-99]";
        }
        UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId());
        return userInfoEntity;
    }
}

  我們只驗證了username和roleId參數,分別驗證爲空驗證及範圍驗證。下面我們測試一下。啓動項目後,訪問以下地址:

http://127.0.0.1:8080/springb...

  我們看一下程序的運行結果。

  title

  因爲我們沒有寫任何參數,所以參數驗證一定是不能通過的。所以就返回的上圖中的提示信息。下面我們看一下數據庫中的數據,然後訪問一下正確的地址,看看能不能成功的返回數據庫中的數據。下圖爲數據庫中的數據:

  title

  下面我們訪問一下正確的參數,然後看一下返回的結果。訪問地址:

http://127.0.0.1:8080/springb...

  訪問結果:

  title

  我們看上圖已經成功的返回數據庫中的數據了,這就是簡單的參數校驗,正是因爲簡單,所以我們就不做過多的介紹了。下面我們簡單分析一下,這樣做參數驗證好不好。如果我們的項目比較簡單,那答案一定是肯定的,因爲站在軟件設計角度考慮,沒必要爲了一個簡單的功能而設計一個複雜的架構。因爲越是複雜的功能,出問題的可能性就越大,程序就越不穩定。但如果站在程序開發角度,那上面的代碼一定是有問題的,因爲上面的代碼根本沒辦法複用,如果要開發很多這樣的項目,要進行參數驗證時,那結果一定是代碼中有很多相類似的代碼,這顯然是不合理的。那怎麼辦呢?那答案就是本篇中的重點內容,也就是SpringBoot對參數的驗證,實際上本篇的內容主要是和Spring內容相關和SpringBoot的關係不大。但SpringBoot中基本包括了所有Spring的內容,所以我們還是以SpringBoot項目爲例。下面我們看一下,怎麼在SpringBoot中的對參數進行校驗。


ObjectError參數校驗

  我們首先看一下代碼,然後在詳細介紹代碼中的新知識。下面爲接受的參數類的源碼。

  修改前:

package com.jilinwula.springboot.helloworld.query;

import lombok.Data;
import org.springframework.stereotype.Component;

@Component
@Data
public class UserInfoQuery{

    private String username;

    private Long roleId;
}

  修改後:

package com.jilinwula.springboot.helloworld.query;

import lombok.Data;
import org.springframework.stereotype.Component;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@Component
@Data
public class UserInfoQuery{

    @NotNull(message = "賬號不能爲空")
    private String username;

    @NotNull(message = "權限不能爲空")
    @Min(value = 1, message = "權限範圍爲[1-99]")
    @Max(value = 99, message = "權限範圍爲[1-99]")
    private Long roleId;
}

  我們看代碼中唯一的區別就是添加了很多的註解。沒錯,在SpringBoot項目中進行參數校驗時,就是使用這些註解來完成的。並且註解的命名很直觀,基本上通過名字就可以知道什麼含義。唯一需要注意的就是這些註解的包是javax中的,而不是其它第三方引入的包。這一點要特別注意,因爲很多第三方的包,也包含這些同名的註解。下面我們繼續看Controller中的改動(備註:有關javax中的校驗註解相關的使用說明,我們後續在做介紹)。Controller源碼:

  改動前:

package com.jilinwula.springboot.helloworld.controller;

import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;
import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;
import com.jilinwula.springboot.helloworld.query.UserInfoQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/userinfo")
public class UserInfoController {

    @Autowired
    private UserInfoRepository userInfoRepository;

    @GetMapping("/query")
    public Object list(UserInfoQuery userInfo) {
        if (StringUtils.isEmpty(userInfo.getUsername())) {
            return "賬號不能爲空";
        }
        if (StringUtils.isEmpty(userInfo.getRoleId()) || userInfo.getRoleId() > 100 || userInfo.getRoleId() < 1) {
            return "權限不能爲空,並且範圍爲[1-99]";
        }
        UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId());
        return userInfoEntity;
    }
}

  改動後:

package com.jilinwula.springboot.helloworld.controller;

import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;
import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;
import com.jilinwula.springboot.helloworld.query.UserInfoQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;


@RestController
@RequestMapping("/userinfo")
public class UserInfoController {

    @Autowired
    private UserInfoRepository userInfoRepository;

    @GetMapping("/query")
    public Object list(@Valid UserInfoQuery userInfo, BindingResult result) {
        if (result.hasErrors()) {
            for (ObjectError error : result.getAllErrors()) {
                return error.getDefaultMessage();
            }
        }
        UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId());
        return userInfoEntity;
    }
}

  我們看代碼改動的還是比較大的首先在入參中添加了@Valid註解。該註解就是標識讓SpringBoot對請求參數進行驗證。也就是和參數類裏的註解是對應的。其次我們修改了直接在Controller中進行參數判斷的邏輯,將以前的代碼修改成了SpringBoot中指定的校驗方式。下面我們啓動項目,來驗證一下上述代碼是否能成功的驗證參數的正確性。我們訪問下面請求地址:

http://127.0.0.1:8080/springb...

  返回結果:

  title

  我們看上圖成功的驗證了爲空的校驗,下面我們試一下範圍的驗證。我們訪問下面的請求地址:

http://127.0.0.1:8080/springb...

  看一下返回結果:

  title

  我們看成功的檢測到了參數範圍不正確。這就是SpringBoot中的參數驗證功能。但上面的代碼一個問題,就是只是會返回錯誤的提示信息,而沒有提示,是哪個參數不正確。下面我們修改一下代碼,來看一下怎麼返回是哪個參數不正確。

FieldError參數校驗

package com.jilinwula.springboot.helloworld.controller;

import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;
import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;
import com.jilinwula.springboot.helloworld.query.UserInfoQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;


@RestController
@RequestMapping("/userinfo")
public class UserInfoController {

    @Autowired
    private UserInfoRepository userInfoRepository;

    @GetMapping("/query")
    public Object list(@Valid UserInfoQuery userInfo, BindingResult result) {
        if (result.hasErrors()) {
            FieldError error = result.getFieldError();
            return error.getField() + "+" + error.getDefaultMessage();
        }
        UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId());
        return userInfoEntity;
    }
}

  我們將獲取ObjectError的類型修改成了FieldError。因爲FieldError類型可以獲取到驗證錯誤的字段名字,所以我們將ObjectError修改爲FieldError。下面我們看一下請求返回的結果。

  title

  我們看這回我們就獲取到了驗證錯誤的字段名子了。在實際的項目開發中,我們在返回接口數據時,大部分都會採用json格式的方式返回,下面我們簡單封裝一個返回的類,使上面的驗證返回json格式。下面爲封裝的返回類的源碼:

package com.jilinwula.springboot.helloworld.utils;

import lombok.Data;

@Data
public class Return {
    private int code;
    private Object data;
    private String msg;

    public static Return error(Object data, String msg) {
        Return r = new Return();
        r.setCode(-1);
        r.setData(data);
        r.setMsg(msg);
        return r;
    }
}

  Controller修改:

package com.jilinwula.springboot.helloworld.controller;

import com.jilinwula.springboot.helloworld.Repository.UserInfoRepository;
import com.jilinwula.springboot.helloworld.entity.UserInfoEntity;
import com.jilinwula.springboot.helloworld.query.UserInfoQuery;
import com.jilinwula.springboot.helloworld.utils.Return;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;


@RestController
@RequestMapping("/userinfo")
public class UserInfoController {

    @Autowired
    private UserInfoRepository userInfoRepository;

    @GetMapping("/query")
    public Object list(@Valid UserInfoQuery userInfo, BindingResult result) {
        if (result.hasErrors()) {
            FieldError error = result.getFieldError();
            return Return.error(error.getField(), error.getDefaultMessage());
        }
        UserInfoEntity userInfoEntity = userInfoRepository.findByUsernameAndRoleId(userInfo.getUsername(), userInfo.getRoleId());
        return userInfoEntity;
    }
}

  我們還是啓動項目,並訪問下面地址看看返回的結果:

http://127.0.0.1:8080/springb...

  返回結果:

  title

創建切面

  這樣我們就返回一個簡單的json類型的數據了。雖然我們的校驗參數的邏輯沒有在Controller裏面寫,但我們還是在Controller裏面寫了很多和業務無關的代碼,並且這些代碼還是重複的,這顯然是不合理的。我們可以將上述相同的代碼的封裝起來,然後統一的處理。這樣就避免了有很多重複的代碼了。那這代碼封裝到哪裏呢?我們可以使用Spring中的切面功能。因爲SpringBoot中基本包括了所有Spring中的技術,所以,我們可以放心大膽的在SpringBoot項目中使用Spring中的技術。我們知道在使用切面技術時,我們可以對方法進行前置增強、後置增強、環繞增強等。這樣我們就可以利用切面的技術,在方法之前,也就是請求Controller之前,做參數的校驗工作,這樣就不會對我們的業務代碼產生侵入了。下面我們看一下切面的源碼然後在做詳細說明:

package com.jilinwula.springboot.helloworld.aspect;

import com.jilinwula.springboot.helloworld.utils.Return;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

@Slf4j
@Aspect
@Component
public class UserAspect {

    @Before("execution(public * com.jilinwula.springboot.helloworld.controller..*(..))")
    public void doBefore(JoinPoint joinPoint) {
        for (Object arg : joinPoint.getArgs()) {
            if (arg instanceof BindingResult) {
                BindingResult result = (BindingResult) arg;
                if (result.hasErrors()) {
                    FieldError error = result.getFieldError();
                    Return.error(error.getField(), error.getDefaultMessage());
                }
            }
        }
    }
}

  我們看上述的代碼中我們添加了一個@Aspect註解,這個就是切面的註解,然後我們在方法中又添加了@Before註解,也就是對目標方法進行前置增強,Spring在請求Controller之前會先請求此方法。所以我們可以將校驗參數的代碼邏輯寫在這個方法中。execution參數爲切點函數,也就是目標方法的切入點。切點函數包含一些通配符的語法,下面我們簡單介紹一下:

    • 匹配任意字符,但它可能匹配上下文中的一個元素
  • .. 匹配任意字符,可以匹配上下文中的多個元素
    • 表示按類型匹配指定類的所有類,必須跟在類名後面,也就是會匹配繼承或者擴展指定類的所有類,包括指定類.

創建異常類

  我們通過上述代碼知道,Spring中的切面功能是沒有返回值的。所以我們在使用切面功能時,是沒有辦法在切面裏面做參數返回的。那我們應該怎麼辦呢?這時異常就派上用場了。我們知道當程序拋出異常時,如果當前方法沒有做try catch處理,那麼異常就會一直向上拋出,如果程序也一直沒有做處理,那麼當前異常就會一直拋出,直到被Java虛擬機捕獲。但Java虛擬機也不會對異常進行處理,而是直接拋出異常。這也就是程序不做任何處理拋出異常的根本原因。我們正好可以利用異常的這種特性,返回參數驗證的結果。因爲在Spring中爲我們提供了統一捕獲異常的方法,我們可以在這個方法中,將我們的異常信息封裝成json格式,這樣我們就可以返回統一的jons格式了。所以在上述的切面中我們手動了拋出了一個異常。該異常因爲我們沒有用任何處理,所以上述異常會被SpringBoot中的統一異常攔截處理。這樣當SpringBoot檢測到參數不正確時,就會拋出一個異常,然後SpringBoot就會檢測到程序拋出的異常,然後返回異常中的信息。下面我們看一下異常類的源碼:

  異常類:

package com.jilinwula.springboot.helloworld.exception;

import com.jilinwula.springboot.helloworld.utils.Return;
import lombok.Data;

@Data
public class UserInfoException extends RuntimeException {
    private Return r;

    public UserInfoException(Return r) {
        this.r = r;
    }
}

  Return源碼:

package com.jilinwula.springboot.helloworld.utils;

import com.jilinwula.springboot.helloworld.exception.UserInfoException;
import lombok.Data;

@Data
public class Return {
    private int code;
    private Object data;
    private String msg;

    public static void error(Object data, String msg) {
        Return r = new Return();
        r.setCode(-1);
        r.setData(data);
        r.setMsg(msg);
        throw new UserInfoException(r);
    }

    public static Return success() {
        Return r = new Return();
        r.setCode(0);
        return r;
    }
}

SpringBoot統一異常攔截

  因爲該異常類比較簡單,我們就不會過多的介紹了,唯一有一點需要注意的是該異常類繼承的是RuntimeException異常類,而不是Exception異常類,原因我們已經在上一篇中介紹了,Spring只會回滾RuntimeException異常類及其子類,而不會回滾Exception異常類的。下面我們看一下Spring中統一攔截異常處理,下面爲該類的源碼:

package com.jilinwula.springboot.helloworld.handler;

import com.jilinwula.springboot.helloworld.exception.UserInfoException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class UserInfoHandler {


    /**
     * 校驗錯誤攔截處理
     *
     * @param e 錯誤信息集合
     * @return 錯誤信息
     */
    @ExceptionHandler(UserInfoException.class)
    public Object handle(UserInfoException e) {
        return e.getR();
    }
}

  我們在該類添加了@RestControllerAdvice註解。該註解就是爲了定義我們統一獲取異常攔截的。然後我們又添加了@ExceptionHandler註解,該註解就是用來攔截異常類的註解,並且可以在當前方法中,直接獲取到該異常類的對象信息。這樣我們直接返回這個異常類的信息就可以了。因爲我們在這個自定義異常類中添加了Return參數,所以,我們只要反悔Return對象的信息即可,而不用返回整個異常的信息。下面我們訪問一下下面的請求,看看上述代碼是否能檢測到參數不正確。請求地址:

http://127.0.0.1:8080/springb...

  返回結果:

  title

  這樣我們完成了參數校驗的功能了,並且這種方式有很大的複用性,即使我們在寫新的Controller,也不需要手動的校驗參數了,只要我們的請求參數是UserInfoQuery類就可以了。還有一點要注意,所以我們不用手動驗證參數了,但我們的請求參數中還是要寫BindingResult參數,這一點要特別注意。


正則表達式校驗註解

  下面我們更詳細的介紹一下參數驗證的註解,我們首先看一下正則校驗,我們在實體類中添加一個新屬性,然後用正則的的方式,驗證該參數的正確性。下面爲實體類源碼:

package com.jilinwula.springboot.helloworld.query;

import lombok.Data;
import org.springframework.stereotype.Component;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

@Component
@Data
public class UserInfoQuery{

    @NotNull(message = "用戶編號不能爲空")
    @Pattern(regexp = "^[1-10]$",message = "用戶編號範圍不正確")
    private String id;

    @NotNull(message = "賬號不能爲空")
    private String username;

    @NotNull(message = "權限不能爲空")
    @Min(value = 1, message = "權限範圍爲[1-99]")
    @Max(value = 99, message = "權限範圍爲[1-99]")
    private Long roleId;
}

  下面我們訪問以下地址:

http://127.0.0.1:8080/springb...

http文件請求接口

  但這回我們不在瀏覽器裏請求,因爲瀏覽器請求不太方便,並且返回的json格式也沒有格式化不方便瀏覽,除非要裝一些瀏覽器插件纔可以。實際上在IDEA中我們可以很方便的請求一下接口地址,並且返回的json內容是自動格式化的。下面我們來看一下怎麼在IDEA中發起接口請求。在IDEA中請求一個接口很簡單,我們只要創建一個.http類型的文件名字就可以。然後我們可以在該文件中,指定我們接口的請求類型,例如GET或者POST。當我們在文件的開口寫GET或者POST時,IDEA會自動有相應的提示。下面我們看一下http文件中的內容。

  http.http:

GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=阿里巴巴&id=-1

  這時標識GET參數的地方,就會出現綠色剪頭,但我們點擊這個綠色箭頭,IDEA就會就會啓動請求GET參數後面的接口。下面我們看一下上述的返回結果。

GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4&id=-1

HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 18 Feb 2019 03:57:29 GMT

{
  "code": -1,
  "data": "id",
  "msg": "用戶編號範圍不正確"
}

Response code: 200; Time: 24ms; Content length: 41 bytes

  這就是.http文件類型的返回結果,用該文件請求接口,相比用瀏覽器來說,要方便的多。因爲我們在實體類中使用正則指定參數範圍爲1-10,所以請求接口時反悔了id參數有錯誤。下面我們輸入一個正確的值在看一下返回結果。

  http.http:

GET http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=阿里巴巴&id=1

  返回結果:


  GET <http://127.0.0.1:8080/springboot/userinfo/query?roleId=3&username=%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4&id=1>

  HTTP/1.1 200

  Content-Type: application/json;charset=UTF-8

  Transfer-Encoding: chunked

  Date: Mon, 18 Feb 2019 05:46:49 GMT

  {

  "id": 61,

  "username": "阿里巴巴",

  "password": "alibaba",

  "nickname": "阿里巴巴",

  "roleId": 3

  }

  Response code: 200; Time: 25ms; Content length: 77 bytes

常見校驗註解

  我們看已經正確的返回數據庫中的數據了。在Spring中,提供了很多種註解來方便我們進行參數校驗,下面是比較常見的註解:

註解 作用
@Null 參數必須爲null
@NotNull 參數必須不爲null
@NotBlank 參數必須不爲null,並且長度必須大於0
@NotEmpty 參數必須不爲空
@Min 參數必須大於等於該值
@Max 參數必須小於等於該值
@Size 參數必須在指定的範圍內
@Past 參數必須是一個過期的時間
@Future 參數必須是一個未來的時間
@Pattern 參數必須滿足正則表達式
@Email 參數必須爲電子郵箱

  上述內容就是SpringBoot中的參數校驗全部內容,如有不正確的歡迎留言,謝謝。


源碼地址

https://github.com/jilinwula/...

原文地址

http://jilinwula.com/article/...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章