SpringBoot(一) 如何實現AOP的許可權控制

範大發表於2019-04-18

Spring AOP是什麼

  1. 最近負責開發一款內部人員使用的日誌管理專案。其中涉及到了人員許可權的校驗問題。於是想到了用spring AOP的思路去實現,避免每次需要手動去新增程式碼校驗。
  2. Spring AOP是什麼,Aspect Oriented Programming, 面向切面程式設計,是Spring的核心之一。面向切面很明顯就是空間意義上的攔截操作。
  3. 比如我需要在每個業務邏輯的前後做些事情,在每次接受請求的時候,寫個日誌
    log.info("=======開始接受請求=======")
    
  4. 如果我希望在所有的介面請求請求的時候,都寫這個日誌。那麼很明顯,我總不能每個介面裡面都加上這個日誌輸出程式碼。無疑是非常繁瑣和重複的。而AOP可以很好的幫助我們去簡化這個冗餘的程式碼。
  5. 面向切面。如果說正常的業務邏輯是水平的,那麼AOP就是垂直的。可以參考X-Y軸的概念。給每個業務邏輯縱向的擴充套件一些功能,將日誌記錄,效能統計,安全控制,事務處理,異常處理等程式碼從業務邏輯程式碼中劃分出來,而這些功能很明顯是可以公用的。
  6. 空間意義上的AOP:
    在這裡插入圖片描述
    在執行正常的業務邏輯時,我們可以利用AOP進行縱向的擴充套件。而不影響它自己的業務邏輯功能。Spring有很多地方都採用了AOP的思想。
  7. 我的這個專案是判斷使用者的執行許可權。具體業務邏輯:
    1)有操作人員進行使用者許可權配置的時候,刪除了某人。往後臺發出刪除使用者的請求。
    2)AOP將該請求攔截,判斷該使用者是否有許可權做該操作。如果有,繼續執行接受請求之後的方法,如果沒有,則返回前端json,表示該使用者無許可權操作。

專案開發歷程

說起來還是非常簡單的。現在我開始說一下我的開發歷程。

  1. Springboot專案中往前端返回特定json字串的相關配置,以及自定義異常的攔截肯定是都要有的。
  2. 實現對許可權的AOP控制,首先要有一個特殊識別符號,不可能對所有的方法都進行許可權控制,只對特定的方法進行許可權控制。所以我先自定義了一個註解 Permission
一、自定義註解Permission
import java.lang.annotation.*;

/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/4/18 5:24 PM
 * @Desc: 自定義許可權註解,用於AOP
 */

@Target( ElementType.METHOD )
@Retention( RetentionPolicy.RUNTIME )
@Documented
public @interface Permission {
    
}
註解的註解:元註解

1. @TARGET

 * 用於標註這個註解放在什麼地方,類上,方法上,構造器上
 * ElementType.METHOD 用於描述方法
 * ElementType.FIELD 用於描述成員變數,物件,屬性(包括enum例項)
 * ElementType.LOCAL_VARIABLE 用於描述區域性變數
 * ElementType.CONSTRUCTOR 用於描述構造器
 * ElementType.PACKAGE 用於描述包
 * ElementType.PARAMETER 用於描述引數
 * ElementType.TYPE 用於描述類,介面,包括(包括註解型別)或enum宣告

2.@Retention

*  用於說明這個註解的生命週期
 * RetentionPolicy.RUNTIME 始終不會丟棄,執行期也保留該註解。因此可以使用反射機制來讀取該註解資訊。
 * 我們自定義的註解通常用這種方式
 * RetentionPolicy.CLASS 在類載入的時候丟棄,在位元組碼檔案的處理中有用。註解預設使用這種方式
 * RetentionPolicy.SOURCE 在編譯階段丟棄,這些註解在編譯結束後就不再有任何意義,所以他們不會寫入位元組碼中
 * @Override,@SuppressWarnings都屬於這類註解。
 * 我們自定義使用中一般使用第一種
 * java過程為 編譯-載入-執行

3. @Documented

 * 將註解資訊新增到文字中

本專案中的Permission註解用於方法,所以我在controller的特定需要許可權控制的方法上新增該註解即可。

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;


/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/4/18 6:17 PM
 * @Desc: 使用者Controller類
 */
@Slf4j
@RestController
@RequestMapping(value = "/user")
@Api(tags = "UserController")
public class UserController {
    
    @Permission
    @ApiOperation( value = "增加使用者", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @RequestMapping(value = "/addUser",method = RequestMethod.POST)
    public Response<Void> addUser(@RequestBody AddUserRequestVO vo){
        /**
         * 具體程式碼實現邏輯省略
         */
        return new Response<>();
    }
}

我使用了swagger2,用於快速構建RESTFUL API,方便除錯。後續我將會攥寫有關swagger的配置。

4. RequestVO

該專案的許可權控制只需要獲取它的許可權識別符號和操作人員ID,然後呼叫寫好的校驗service去執行校驗方法即可。
於是我定義了一個RequestVO

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/4/18 6:18 PM
 * @Desc: 許可權控制的請求vo基類
 */
@Setter
@Getter
@NoArgsConstructor
public class RequestVO {

    /**
     * 許可權識別符號
     */
    private String authrity;

    /**
     * 操作人員ID
     */
    private Long adminId;
}

讓所有的需要許可權控制的介面,請求vo全部繼承這個控制許可權VO基類。
在獲取攔截的方法中的引數之後,直接去呼叫service方法校驗即可。

二、AOP配置

先貼程式碼

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/4/18 6:46 PM
 * @Desc:
 */
@Slf4j
@Aspect
@Component
@ResponseBody
public class PermissionAspect {
	@Autowired
    private CheckService checkService;

    @Around( value = "@annotation(permission)")
    public Response PermissionCheck(ProceedingJoinPoint joinPoint, Permission permission) throws Throwable{
        log.info("======開始許可權校驗======");

        //1.獲取請求引數
        Object[] objects = joinPoint.getArgs();

        for(Object obj : objects){
            if (obj instanceof RequestVO){
                Long adminId = ((RequestVO) obj).getAdminId();
                String authority = ((RequestVO) obj).getAuthrity();

                //若校驗失敗,丟擲自定義異常
                if (checkService.check( adminId,authority )){
                    log.info( "=======許可權校驗失敗======" );

                    throw new BusinessException( "抱歉,您無該操作許可權" );
                }

                log.info( "=======許可權校驗成功======" );
                //若校驗成功,繼續方法的執行,並獲取返回結果,返回給前端
                try {
                    Object object = joinPoint.proceed();
                    if (object instanceof Response){
                        return (Response)object;
                    }

                } catch (Throwable throwable) {
                    /**
                     *  在方法執行過程中,捕獲異常
                     *  如果捕獲的是自定義異常,則取出內容並丟擲
                     *  如果捕獲的不是自定義異常,直接丟擲
                     *  
                     */
                    if(throwable instanceof BusinessException){
                        throw new BusinessException( throwable.getMessage() );
                    }
                    throw new Exception( throwable );
                }
            }
        }
        return new Response(  );
    }
}

這一串程式碼基本上就是我專案中整個AOP的配置了。

  1. 基本註解
@Aspect : 將當前類標識為一個切面
@Component :肯定是必不可少的。讓Spring容器掃描到。
@ResponseBody 在我的專案中,在後續的許可權校驗後,會返回特定的json物件給前端,所以此處加了該註解。視具體專案而論
  1. 方法註解
1)@Before 前置通知,在方法執行之前
2)@After 後置通知,在方法執行之後
3)@Around 環繞通知,在方法執行之前執行之後都可以。也是我的程式碼中使用的
裡面的格式
	@Around( value = "@annotation(xxx)")即可 xxx為你的自定義註解名
使用該註解,方法引數中第一個引數必須是 ProceedingJoinPoint
4)@Pointcut 定義切點

3.通過 ProceedingJoinPoint獲取方法引數

Object[] getArgs() 獲取方法引數
Signature getSignature() :獲取方法簽名物件; (後跟.getName 即可獲取方法名)
Object getTarget:獲取目標物件

  1. 獲取引數後,本來是用的
 Arrays.asList(objects).stream().forEach( object -> {} );

可是由於其中不能直接方法返回,所以只能用for迴圈迭代陣列。
如果有更好的實現方法,歡迎留言提出。
5. 直接校驗許可權,如果許可權校驗失敗,直接返回自定義異常。如果校驗成功,繼續執行介面中的方法。

Object object = oinPoint.proceed();

該方法是需要加上try…catch的。可是加上去之後,預設catch的異常是
Throwable 。在介面方法具體實現中丟擲的自定義異常,可能就無法被我的異常捕獲器捕獲。
所以先判斷捕獲的異常是否是自定義異常,如果是,就繼續丟擲我的自定義異常。如果不是,則丟擲預設的Exception。

  if(throwable instanceof BusinessException){
      throw new BusinessException( throwable.getMessage() );
  }
     throw new Exception( throwable );

Object即是介面方法繼續執行後的返回值。
專案中我封裝了一個Response,專門用於與前端互動。

import org.springframework.http.HttpStatus;

@Setter
@Getter
public class Response<T> {

    private String code;

    private String message;

    private String updateTime;

    private T body;


    public Response code(String code) {
        this.code = code;
        return this;
    }

    public Response body(T body) {
        this.body = body;
        return this;
    }

    public Response message(String message) {
        this.message = message;
        return this;
    }

    public Response time(String updateTime) {
        this.updateTime = updateTime;
        return this;
    }

    /**
     * 該構造方法預設code 為200
     */
    public Response() {
        this(HttpStatus.OK.name(), null);
    }

    /**
     * 該構造方法預設code 為200
     * @param body 需要返回的物件
     */
    public Response(T body) {
        this(HttpStatus.OK.name(), body);
    }

    public Response(String code, T body) {
        this(code, null, body);
    }

    public Response(String code, String message, T body) {
        this.code = code;
        this.body = body;
        this.message = message;
    }
}

我的所有介面返回值都是封裝為Response,我只需要判斷一下object是否是我的Response類,即可直接返回給前端。

Object object = joinPoint.proceed();
if (object instanceof Response){
   return (Response)object;
}

以上就是我專案中的AOP實現了。
通過自定義註解的方式,去動態的控制部分介面方法執行AOP許可權控制。
總的來說,收穫還是很大的。

後續:
1. 在研究了aop的註解之後,發現@Around並不適合我的這個許可權校驗。用@Before更加簡單一些,也不需要再去處理方法處理後的情況。
於是修改了一下:

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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/4/18 6:46 PM
 * @Desc:
 */
@Slf4j
@Aspect
@Component
public class PermissionAspect {

    @Autowired
    private CheckService checkService;

    @Before( value = "@annotation(permission)")
    public void PermissionCheck(JoinPoint joinPoint, Permission permission){
        log.info("======開始許可權校驗======");

        //1.獲取請求引數
        Object[] objects = joinPoint.getArgs();
        for(Object obj : objects){
            if (obj instanceof RequestVO){
                Long adminId = ((RequestVO) obj).getAdminId();
                String authority = ((RequestVO) obj).getAuthrity();

                //若校驗失敗,丟擲自定義異常
                if (checkService.check( adminId,authority )){
                    log.info( "=======許可權校驗失敗======" );
                    throw new BusinessException( "抱歉,您無該操作許可權" );
                }

                log.info( "=======許可權校驗成功======" );
            }
        }
    }

}

直接用@Before只需要關心方法執行前的許可權校驗即可。後續的請求處理就不需要管了。

  1. @Pointcut的使用
    如果不使用自定義註解的方式去控制哪些方法或類經過你的aop控制,也可以直接定義Pointcut(切點)。
    程式碼如下:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.Arrays;

/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/1/28 6:13 PM
 * @Desc: AOP實現列印介面呼叫日誌
 */

@Aspect
@Slf4j
@Component
public class LogAspect {

    /**
     * 定義切點,這是一個標記方法
     * com.xxx.xxx.service下的所有子包及方法
     */
    @Pointcut("execution( * com.xxx.xxx.service..*.*(..))")
    public void anyMethod() {
    }

    @Before( "anyMethod()" )
    public void Before(JoinPoint joinPoint){
        log.info( "========接受到請求========" );
    }

    @AfterReturning("anyMethod()")
    public void afterMethod(){
        log.info( "=======請求處理完畢========" );
    }

    @AfterThrowing("anyMethod()")
    public void afterThrowMethod(){
        log.info( "=======請求處理異常========" );
    }

   }

定義某一個地方為切點,裡面的語法可以自己網上搜尋,可以直接標識到某個包及包下的所有子類。只在你的切點範圍內,會執行AOP對應操作。也是很方便的

這個時候 @Before @After中的註解範圍就是你的切點方法了
@After 方法執行完後通知(不論是執行成功還是異常)
@AfterReturning 方法正常執行後通知
@AfterThrowing 方法丟擲異常後通知

不管是用切點還是自定義註解的方式,都可以控制AOP執行的範圍。視專案而定即可。

ProceedingJoinPoint extends JoinPoint

具體差異大家可以看原始碼。

第一次寫部落格,各位大牛多多包涵哈。歡迎留言評論?

相關文章