手把手0基礎專案實戰(三)——教你開發一套電商平臺的安全框架

大閒人柴毛毛發表於2018-03-11

寫在最前

本文是《手把手專案實戰系列》的第三篇文章,預告一下,整個系列會介紹如下內容:

  • 《手把手0基礎專案實戰(一)——教你搭建一套可自動化構建的微服務框架(SpringBoot+Dubbo+Docker+Jenkins)》
  • 《手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰》
  • 《手把手0基礎專案實戰(三)——教你開發一套安全框架》
  • 《手把手0基礎專案實戰(四)——電商訂單系統架構設計與實戰(分散式事務一致性保證)》
  • 《手把手0基礎專案實戰(五)——電商系統的快取策略》
  • 《手把手0基礎專案實戰(六)——基於配置中心實現叢集配置的集中管理和熔斷機制》
  • 《手把手0基礎專案實戰(七)——電商系統的日誌監控方案》
  • 《手把手0基礎專案實戰(八)——基於JMeter的系統效能測試》

手把手0基礎專案實戰(三)——教你開發一套電商平臺的安全框架

幾乎所有的Web系統都需要登入、許可權管理、角色管理等功能,而且這些功能往往具有較大的普適性,與系統具體的業務關聯性較小。因此,這些功能完全可以被封裝成一個可配置、可插拔的框架,當開發一個新系統的時候直接將其引入、並作簡單配置即可,無需再從頭開發,極大節約了人力成本、時間成本。

在Java Web領域,有兩大主流的安全框架,Spring Security和Apache Shiro。他們都能實現使用者鑑權、許可權管理、角色管理、防止Web攻擊等功能,而且這兩套開源框架都已經過大量專案的驗證,趨於穩定成熟,可以很好地為我們的專案服務。

本文將帶領大家從頭開始實現一套安全框架,該框架與Spring Boot深度融合,從而能夠幫助大家加深對Spring Boot的理解。這套框架中將涉及到如下內容:

  • Spring Boot AOP
  • Spring Boot 全域性異常處理
  • Spring Boot CommandLineRunner
  • Java 反射機制
  • 分散式系統中Session的集中式管理

本文將從安全框架的設計與實現兩個角度帶領大家完成安全框架的開發,廢話不多說,現在開始吧~

專案完整原始碼下載

https://github.com/bz51/SpringBoot-Dubbo-Docker-Jenkins


1. 安全框架的設計

1.1 開發目標

在所有事情開始之前,我們首先要搞清楚,我們究竟要實現哪些功能?

  1. 使用者登入 所有系統都需要登入功能,這毫無疑問,也不必多說。
  2. 角色管理 每個使用者都有且僅有一種角色,比如:系統管理員、普通使用者、企業使用者等等。管理員可以新增、刪除、查詢、修改角色資訊。
  3. 許可權管理 每種角色可以擁有不同的許可權,管理員可以建立、修改、查詢、刪除許可權,也可以為某一種角色新增、刪除許可權。
  4. 許可權檢測 使用者呼叫每一個介面,都需要校驗該使用者是否具備呼叫該介面的許可權。

當我們明確了開發目標之後,下面就需要基於這些目標,設計我們的系統。我們首先要做的就是要搞清楚“使用者”、“角色”、“許可權”的定義以及他們之間的關係。這在領域驅動設計中被稱為“領域模型”。

1.2 領域模型

title

  • 許可權:
    • 許可權表示某一使用者是否具有操作某一資源的能力。
    • 許可權一般用“資源名稱:操作名稱”來表示。比如:建立使用者的許可權可以用“user:create”來表示,刪除使用者的許可權可以用“user:delete”來表示。
    • 在Web系統中,許可權和介面呈一一對應關係,比如:“user:create”對應著建立使用者的介面,“user:delete”對應著刪除使用者的介面。因此,許可權也可以理解成一個使用者是否具備操作某一個介面的能力。
  • 角色:
    • 角色是一組許可權的集合。角色規定了某一類使用者共同具備的許可權集合。
    • 比如:超級管理員這種角色擁有“user:create”、“user:delete”等許可權,而普通使用者只有“user:create”許可權。
    • 從領域模型中可知,角色和許可權之間呈多對多的聚合關係,即一種角色可以包含多個許可權,一個許可權也可以屬於多種角色,並且許可權可以脫離於角色而單獨存在,因此他們之間是一種弱依賴關係——聚合關係。
  • 使用者:
    • 使用者和角色之間呈多對一的聚合關係,即一個使用者只能屬於一種角色,而一種角色卻可以包含多個使用者。並且角色可以脫離於使用者單獨存在,因此他們之間是一種弱依賴關係——聚合關係。

1.3 資料結構設計

當我們捋清楚了“許可權”、“使用者”、“角色”的定義和他們之間的關係後,下面我們就可以基於這個領域模型設計出具體的資料儲存結構。

為了能夠方便地給每一個介面標註許可權,我們需要自定義三個註解@Login@Role@Permission

  • @Login:用於標識當前介面是否需要登入。當介面使用了這個註解後,使用者只有在登入後才能訪問。

  • @Role("角色名"):用於標識允許呼叫當前介面的角色。當介面使用了這個註解後,只有指定角色的使用者才能呼叫本介面。

  • @Permission("許可權名"):用於標識允許呼叫當前介面的許可權。當介面使用了這個註解後,只有具備指定許可權的使用者才能呼叫本介面。

1.4 介面許可權資訊初始化流程

要使得這個安全框架執行起來,首先就需要在系統初始化完成前,初始化所有介面的許可權、角色等資訊,這個過程即為“介面許可權資訊初始化流程”;然後在系統執行期間,如果有使用者請求介面,就可以根據這些許可權資訊判斷該使用者是否有許可權訪問介面。

這一小節主要介紹介面許可權資訊初始化流程,不涉及任何實現細節,實現的細節將在本文的實現部分介紹。

  1. 當Spring完成上下文的初始化後,需要掃描本專案中所有Controller類;
  2. 再依次掃描Controller類中的所有方法,獲取方法上的@GetMapping@PostMapping@PutMapping@DeleteMapping,通過這些註解獲取介面的URL、請求方式等資訊;
  3. 同時,獲取方法上的@Login@Role@Permission,通過這些註解,獲取該介面是否需要登入、允許訪問的角色以及允許訪問的許可權資訊;
  4. 將每個介面的許可權資訊、URL、請求方式儲存在Redis中,供使用者呼叫介面是鑑權使用。

1.5 使用者鑑權流程

  1. 所有的使用者請求在被執行前都會被系統攔截,從請求中獲取請求的URL和請求方式;
  2. 然後從Redis中查詢該介面對應的許可權資訊;
  3. 若該介面需要登入,並且當前使用者尚未登入,則直接拒絕;
  4. 若該介面需要登入,並且擁有已經登入,那麼需要從請求頭中解析出SessionID,併到Redis中查詢該使用者的許可權資訊,然後拿著使用者的許可權資訊、角色資訊和該介面的許可權資訊、角色資訊進行比對。若通過鑑權,則執行該介面;若未通過鑑權,則直接拒絕請求。

2. 安全框架的實現

2.1 註解的實現

本套安全框架一共定義了四個註解:@AuthScan@Login@Role@Permission

2.1.1 @AuthScan

該註解用來告訴安全框架,本專案中所有Controller類所在的包,從而能夠幫助安全框架快速找到Controller類,避免了所有類的掃描。

它有且僅有一個引數,用來指定Controller所在的包:@AuthScan("com.gaoxi.controller")。它的程式碼實現如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthScan {
    public String value();
}
複製程式碼

註解顧名思義,它是用來在程式碼中進行標註,它本身不承載任何邏輯,通過註解

  • @Retention 它解釋說明了這個註解的的存活時間。它的取值如下:

    • RetentionPolicy.SOURCE 註解只在原始碼階段保留,在編譯器進行編譯時它將被丟棄忽視。
    • RetentionPolicy.CLASS 註解只被保留到編譯進行的時候,它並不會被載入到 JVM 中。
    • RetentionPolicy.RUNTIME 註解可以保留到程式執行的時候,它會被載入進入到 JVM 中,所以在程式執行時可以獲取到它們。
  • @Documented 顧名思義,這個元註解肯定是和文件有關。它的作用是能夠將註解中的元素包含到 Javadoc 中去。

  • @Target 當一個註解被 @Target 註解時,這個註解就被限定了運用的場景。

    • ElementType.ANNOTATION_TYPE:可以給一個註解進行註解
    • ElementType.CONSTRUCTOR:可以給構造方法進行註解
    • ElementType.FIELD:可以給屬性進行註解
    • ElementType.LOCAL_VARIABLE:可以給區域性變數進行註解
    • ElementType.METHOD:可以給方法進行註解
    • ElementType.PACKAGE:可以給一個包進行註解
    • ElementType.PARAMETER:可以給一個方法內的引數進行註解
    • ElementType.TYPE:可以給一個型別進行註解,比如類、介面、列舉

2.1.2 @Login

這個註解用於標識指定介面是否需要登入後才能訪問,它有一個預設的boolean型別的值,用於表示是否需要登入,其程式碼如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Login {

    // 是否需要登入(預設為true)
    public boolean value() default true;

}
複製程式碼

2.1.3 @Role

該註解用於指定允許訪問當前介面的角色,其程式碼如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Role {
    public String value();
}
複製程式碼

2.1.4 @Permission

該註解用於指定允許訪問當前介面的許可權,其程式碼如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Permission {
    public String value();
}
複製程式碼

2.2 許可權資訊初始化過程

上文中提到,註解本身不含任何業務邏輯,它只是在程式碼中起一個標識的作用,那麼怎麼才能讓註解“活”起來?這就需要通過反射機制來獲取註解。

2.2.1 在介面上宣告許可權資訊

當完成這些註解的定義後,接下來就需要使用他們,如下面程式碼所示:

public interface ProductController {

    /**
     * 建立產品
     * @param prodInsertReq 產品詳情
     * @return 是否建立成功
     */
    @PostMapping("product")
    @Login
    @Permission("product:create")
    public Result createProduct(ProdInsertReq prodInsertReq);
    
}
複製程式碼

ProductController是一個Controller類,它提供了處理產品的各種介面。簡單起見,這裡只列出了一個建立產品的介面。 @PostMapping是SpringMVC提供的註解,用於標識該介面的訪問路徑和訪問方式。 @Login宣告瞭該介面需要登入後才能訪問。 @Permission宣告瞭使用者只有擁有product:create許可權才能訪問該介面。

2.2.2 初始化許可權資訊

當系統初始化的時候,需要載入介面上的這些許可權資訊,儲存在Redis中。在系統執行期間,當有使用者請求介面的時候,系統會根據介面的許可權資訊判斷使用者是否有訪問介面的許可權。許可權資訊初始化過程的程式碼如下:

/**
 * @author 大閒人柴毛毛
 * @date 2017/11/1 上午10:04
 *
 * @description 初始化許可權資訊
 */
@AuthScan("com.gaoxi.controller")
@Component
public class InitAuth implements CommandLineRunner {
    @Override
    public void run(String... strings) throws Exception {
        // 載入介面訪問許可權
        loadAccessAuth();
    }
    
    ……
}
複製程式碼
  • 上述程式碼定義了一個InitAuth類,該類實現了CommandLineRunner介面,該介面中含有run()方法,當Spring的上下文初始化完成後,就會呼叫run(),從而完成許可權資訊的初始化過程。
  • 該類使用了@AuthScan("com.gaoxi.controller")註解,用於標識當前專案Controller類所在的包名,從而避免掃描所有類,一定程度上加速系統初始化的速度。
  • @Component註解會在Spring容器初始化完成後,建立本類的物件,並加入IoC容器中。

下面來看一下loadAccessAuth()方法的具體實現:

    /**
     * 載入介面訪問許可權
     */
    private void loadAccessAuth() throws IOException {
        // 獲取待掃描的包名
        AuthScan authScan = AnnotationUtil.getAnnotationValueByClass(this.getClass(), AuthScan.class);
        String pkgName = authScan.value();

        // 獲取包下所有類
        List<Class<?>> classes = ClassUtil.getClasses(pkgName);
        if (CollectionUtils.isEmpty(classes)) {
            return;
        }

        // 遍歷類
        for (Class clazz : classes) {
            Method[] methods = clazz.getMethods();
            if (methods==null || methods.length==0) {
                continue;
            }

            // 遍歷函式
            for (Method method : methods) {
                AccessAuthEntity accessAuthEntity = buildAccessAuthEntity(method);
                if (accessAuthEntity!=null) {
                    // 生成key
                    String key = generateKey(accessAuthEntity);
                    // 存至本地Map
                    accessAuthMap.put(key, accessAuthEntity);
                    logger.debug("",accessAuthEntity);
                }
            }
        }
        // 存至Redis
        redisService.setMap(RedisPrefixUtil.Access_Auth_Prefix, accessAuthMap, null);
        logger.info("介面訪問許可權已載入完畢!"+accessAuthMap);
    }
複製程式碼
  • 首先會讀取本類上的@AuthScan註解,並獲取註解中宣告瞭Controller類所在的包pkgName
  • pkgName是一個字串,因此需要使用Java反射機制將字串解析成Class物件。其解析過程通過工具包ClassUtil.getClasses(pkgName)完成,具體解析過程這裡就不做詳細介紹了,感興趣的同學可以參閱本專案原始碼。
  • ClassUtil.getClasses(pkgName)解析之後,該包下的所有Controller類將會被解析成List<Class<?>>物件,然後遍歷所有的Class物件;
  • 然後依次獲取每個Class物件中的Method物件,並依次遍歷Method物件,通過buildAccessAuthEntity(method)方法將一個個Method物件解析成AccessAuthEntity物件(具體解析過程在稍後介紹);
  • 最後將AccessAuthEntity物件儲存在Redis中,供使用者訪問介面時使用。

這就是整個許可權資訊初始化的過程,下面詳細介紹buildAccessAuthEntity(method)方法的解析過程,它究竟是如何將一個Mehtod物件解析成AccessAuthEntity物件?並且AccessAuthEntity物件的結構究竟是怎樣的?

首先來看一下AccessAuthEntity的資料結構:

/**
 * @author 大閒人柴毛毛
 * @date 2017/11/1 上午11:05
 * @description 介面訪問許可權的實體類
 */
public class AccessAuthEntity implements Serializable {

    /** 請求 URL */
    private String url;

    /** 介面方法名 */
    private String methodName;

    /** HTTP 請求方式 */
    private HttpMethodEnum httpMethodEnum;

    /** 當前介面是否需要登入 */
    private boolean isLogin;

    /** 當前介面的訪問許可權 */
    private String permission;
    
    // setter/getter省略
}
複製程式碼

AccessAuthEntity用於儲存一個介面的訪問路徑、訪問方式和許可權資訊。在系統初始化的時候,Controller類中的每個Mehtod物件都會被buildAccessAuthEntity()方法解析成AccessAuthEntity物件。buildAccessAuthEntity()方法的程式碼如下所示:

/**
 * 構造AccessAuthEntity物件
 * @param method
 * @return
 */
private AccessAuthEntity buildAccessAuthEntity(Method method) {
    GetMapping getMapping = AnnotationUtil.getAnnotationValueByMethod(method, GetMapping.class);
    PostMapping postMapping = AnnotationUtil.getAnnotationValueByMethod(method, PostMapping.class);
    PutMapping putMapping= AnnotationUtil.getAnnotationValueByMethod(method, PutMapping.class);
    DeleteMapping deleteMapping = AnnotationUtil.getAnnotationValueByMethod(method, DeleteMapping.class);

    AccessAuthEntity accessAuthEntity = null;
    if (getMapping!=null
            && getMapping.value()!=null
            && getMapping.value().length==1
            && StringUtils.isNotEmpty(getMapping.value()[0])) {
        accessAuthEntity = new AccessAuthEntity();
        accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.GET);
        accessAuthEntity.setUrl(trimUrl(getMapping.value()[0]));
    }
    else if (postMapping!=null
            && postMapping.value()!=null
            && postMapping.value().length==1
            && StringUtils.isNotEmpty(postMapping.value()[0])) {
        accessAuthEntity = new AccessAuthEntity();
        accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.POST);
        accessAuthEntity.setUrl(trimUrl(postMapping.value()[0]));
    }
    else if (putMapping!=null
            && putMapping.value()!=null
            && putMapping.value().length==1
            && StringUtils.isNotEmpty(putMapping.value()[0])) {
        accessAuthEntity = new AccessAuthEntity();
        accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.PUT);
        accessAuthEntity.setUrl(trimUrl(putMapping.value()[0]));
    }
    else if (deleteMapping!=null
            && deleteMapping.value()!=null
            && deleteMapping.value().length==1
            && StringUtils.isNotEmpty(deleteMapping.value()[0])) {
        accessAuthEntity = new AccessAuthEntity();
        accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.DELETE);
        accessAuthEntity.setUrl(trimUrl(deleteMapping.value()[0]));
    }

    // 解析@Login 和 @Permission
    if (accessAuthEntity!=null) {
        accessAuthEntity = getLoginAndPermission(method, accessAuthEntity);
        accessAuthEntity.setMethodName(method.getName());
    }

    return accessAuthEntity;
}
複製程式碼

該方法首先會獲取當前Method上的XXXMapping四個註解,通過解析這些註解能夠獲取到當前介面的訪問路徑和請求方式,並將這兩者儲存在AccessAuthEntity物件中。

然後通過getLoginAndPermission方法,解析當前Method物件中的@Login 和@Permission資訊,其程式碼如下所示:

/**
 * 獲取指定方法上的@Login的值和@Permission的值
 * @param method 目標方法
 * @param accessAuthEntity
 * @return
 */
private AccessAuthEntity getLoginAndPermission(Method method, AccessAuthEntity accessAuthEntity) {
    // 獲取@Permission的值
    Permission permission = AnnotationUtil.getAnnotationValueByMethod(method, Permission.class);
    if (permission!=null && StringUtils.isNotEmpty(permission.value())) {
        accessAuthEntity.setPermission(permission.value());
        accessAuthEntity.setLogin(true);
        return accessAuthEntity;
    }

    // 獲取@Login的值
    Login login = AnnotationUtil.getAnnotationValueByMethod(method, Login.class);
    if (login!=null) {
        accessAuthEntity.setLogin(true);
    }

    accessAuthEntity.setLogin(false);
    return accessAuthEntity;
}
複製程式碼

該註解的解析過程由註解工具包AnnotationUtil.getAnnotationValueByMethod完成,具體的解析過程這裡就不再贅述,感興趣的同學請參閱專案原始碼。

到此為止,介面的訪問路徑、請求方式、是否需要登入、許可權資訊都已經解析成一個個AccessAuthEntity物件,並以“請求方式+訪問路徑”作為key,儲存在Redis中。介面許可權資訊的初始化過程也就完成了!

2.2.3 使用者鑑權

當使用者請求所有介面前,系統都應該攔截這些請求,只有在許可權校驗通過的情況下才執行呼叫介面,否則直接拒絕請求。

基於上述需求,我們需要給Controller中所有方法執行前增加切面,並將用於許可權校驗的程式碼織入到該切面中,從而在方法執行前完成許可權校驗。下面就詳細介紹在SpringBoot中AOP的使用。

  • 首先,我們需要在專案的pom中引入AOP的依賴:
<!-- AOP -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
複製程式碼
  • 建立切面類:
    • 在類上必須新增@Aspect註解,用於標識當前類是一個AOP切面類
    • 該類也必須新增@Component註解,讓Spring初始化完成後建立本類的物件,並加入IoC容器中
    • 然後需要使用@Pointcut註解定義切點;切點描述了哪些類中的哪些方法需要織入許可權校驗程式碼。我們這裡將所有Controller類中的所有方法作為切點。
    • 當完成切點的定義後,我們需要使用@Before註解宣告切面織入的時機;由於我們需要在方法執行前攔截所有的請求,因此使用@Before註解。
    • 當完成上述設定之後,所有Controller類中的函式在被呼叫前,都會執行許可權校驗程式碼。許可權校驗的詳細過程在authentication()方法中完成。
/**
 * @author 大閒人柴毛毛
 * @date 2017/11/2 下午7:06
 *
 * @description 訪問許可權處理類(所有請求都要經過此類)
 */
@Aspect
@Component
public class AccessAuthHandle {
    /** 定義切點 */
    @Pointcut("execution(public * com.gaoxi.controller..*.*(..))")
    public void accessAuth(){}


    /**
     * 攔截所有請求
     */
    @Before("accessAuth()")
    public void doBefore() {

        // 訪問鑑權
        authentication();

    }
}
複製程式碼
  • 許可權校驗過程
    • 該方法首先會獲取當前請求的訪問路徑和請求方法;
    • 然後獲取HTTP請求頭中的SessionID,並從Redis中獲取該SessionID對應的使用者資訊;
    • 然後根據介面訪問路徑和訪問方法,從Redis中獲取該介面的許可權資訊;到此為止,許可權校驗前的準備工作都已完成,下面就要進入許可權校驗過程了;
/**
 * 檢查當前使用者是否允許訪問該介面
 */
private void authentication() {
    // 獲取 HttpServletRequest
    HttpServletRequest request = getHttpServletRequest();

    // 獲取 method 和 url
    String method = request.getMethod();
    String url = request.getServletPath();

    // 獲取 SessionID
    String sessionID = getSessionID(request);

    // 獲取SessionID對應的使用者資訊
    UserEntity userEntity = getUserEntity(sessionID);

    // 獲取介面許可權資訊
    AccessAuthEntity accessAuthEntity = getAccessAuthEntity(method, url);

    // 檢查許可權
    authentication(userEntity, accessAuthEntity);
}
複製程式碼
  • authentication():
    • 首先判斷當前介面是否需要登入後才允許訪問,如果無需登入,那麼直接允許訪問;
    • 若當前介面需要登入後才能訪問,那麼判斷當前使用者是否已經登入;若尚未登入,則直接拒絕請求(通過丟擲throw new CommonBizException(ExpCodeEnum.NO_PERMISSION)異常來拒絕請求,這由SpringBoot統一異常處理機制來完成,稍後會詳細介紹);若已經登入,則開始檢查許可權資訊;
    • 許可權檢查由checkPermission()方法完成,它會將使用者所具備的許可權和介面要求的許可權進行比對;如果使用者所具備的許可權包含介面要求的許可權,那麼許可權校驗通過;反之,則通過拋異常的方式拒絕請求。
/**
 * 檢查許可權
 * @param userEntity 當前使用者的資訊
 * @param accessAuthEntity 當前介面的訪問許可權
 */
private void authentication(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
    // 無需登入
    if (!accessAuthEntity.isLogin()) {
        return;
    }

    // 檢查是否登入
    checkLogin(userEntity, accessAuthEntity);

    // 檢查是否擁有許可權
    checkPermission(userEntity, accessAuthEntity);
}

/**
 * 檢查當前使用者是否擁有訪問該介面的許可權
 * @param userEntity 使用者資訊
 * @param accessAuthEntity 介面許可權資訊
 */
private void checkPermission(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
    // 獲取介面許可權
    String accessPermission = accessAuthEntity.getPermission();

    // 獲取使用者許可權
    List<PermissionEntity> userPermissionList = userEntity.getRoleEntity().getPermissionList();

    // 判斷使用者是否包含介面許可權
    if (CollectionUtils.isNotEmpty(userPermissionList)) {
        for (PermissionEntity permissionEntity : userPermissionList) {
            if (permissionEntity.getPermission().equals(accessPermission)) {
                return;
            }
        }
    }

    // 沒有許可權
    throw new CommonBizException(ExpCodeEnum.NO_PERMISSION);
}


/**
 * 檢查當前介面是否需要登入
 * @param userEntity 使用者資訊
 * @param accessAuthEntity 介面訪問許可權
 */
private void checkLogin(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
    // 尚未登入
    if (accessAuthEntity.isLogin() && userEntity==null) {
        throw new CommonBizException(ExpCodeEnum.UNLOGIN);
    }
}
複製程式碼
  • 全域性異常處理 為了是得程式碼具備良好的可讀性,這裡使用了SpringBoot提供的全域性異常處理機制。我們只需丟擲異常即可,這些異常會被我們預先設定的全域性異常處理類捕獲並處理。全域性異常處理本質上藉助於AOP完成。
    • 我們需要定義全域性異常處理類,它只是一個普通類,我們只要用@ControllerAdvice註解宣告即可
    • 我們還需要在這個類上增加@ResponseBody註解,它能夠幫助我們當處理完異常後,直接向使用者返回JSON格式的錯誤資訊,而無需我們手動處理。
    • 在這個類中,我們根據異常型別不同,定義了兩個異常處理函式,分別用於捕獲業務異常、系統異常。並且需要使用@ExceptionHandler註解告訴Spring,該方法用於處理什麼型別的異常。
    • 當我們完成上述配置後,只要專案中任何地方丟擲異常,都會被這個全域性異常處理類捕獲,並根據丟擲異常的型別選擇相應的異常處理函式。
/**
 * @Author 大閒人柴毛毛
 * @Date 2017/10/27 下午11:02
 * REST介面的通用異常處理
 */
@ControllerAdvice
@ResponseBody
public class ExceptionHandle {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 業務異常處理
     * @param exception
     * @param <T>
     * @return
     */
    @ExceptionHandler(CommonBizException.class)
    public <T> Result<T> exceptionHandler(CommonBizException exception) {
        return Result.newFailureResult(exception);
    }

    /**
     * 系統異常處理
     * @param exception
     * @return
     */
    @ExceptionHandler(Exception.class)
    public <T> Result<T> sysExpHandler(Exception exception) {
        logger.error("系統異常 ",exception);
        return Result.newFailureResult();
    }
}
複製程式碼

手把手0基礎專案實戰(三)——教你開發一套電商平臺的安全框架

相關文章