SpringBoot參數校驗

本篇概述

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


手動參數校驗

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

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參數,分別驗證爲空驗證及範圍驗證。下面咱們測試一下。啓動項目後,訪問如下地址:github

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

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

  title

  由於咱們沒有寫任何參數,因此參數驗證必定是不能經過的。因此就返回的上圖中的提示信息。下面咱們看一下數據庫中的數據,而後訪問一下正確的地址,看看能不能成功的返回數據庫中的數據。下圖爲數據庫中的數據:正則表達式

  title

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

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

  訪問結果:數據庫

  title

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


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/...
相關文章
相關標籤/搜索