成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

手把手0基礎(chǔ)項(xiàng)目實(shí)戰(zhàn)(三)——教你開發(fā)一套電商平臺(tái)的安全框架

RaoMeng / 913人閱讀

摘要:在領(lǐng)域,有兩大主流的安全框架,和。角色角色是一組權(quán)限的集合。安全框架的實(shí)現(xiàn)注解的實(shí)現(xiàn)本套安全框架一共定義了四個(gè)注解。該注解用來告訴安全框架,本項(xiàng)目中所有類所在的包,從而能夠幫助安全框架快速找到類,避免了所有類的掃描。

寫在最前

本文是《手把手項(xiàng)目實(shí)戰(zhàn)系列》的第三篇文章,預(yù)告一下,整個(gè)系列會(huì)介紹如下內(nèi)容:

《手把手0基礎(chǔ)項(xiàng)目實(shí)戰(zhàn)(一)——教你搭建一套可自動(dòng)化構(gòu)建的微服務(wù)框架(SpringBoot+Dubbo+Docker+Jenkins)》

《手把手0基礎(chǔ)項(xiàng)目實(shí)戰(zhàn)(二)——微服務(wù)架構(gòu)下的數(shù)據(jù)庫(kù)分庫(kù)分表實(shí)戰(zhàn)》

《手把手0基礎(chǔ)項(xiàng)目實(shí)戰(zhàn)(三)——教你開發(fā)一套安全框架》

《手把手0基礎(chǔ)項(xiàng)目實(shí)戰(zhàn)(四)——電商訂單系統(tǒng)架構(gòu)設(shè)計(jì)與實(shí)戰(zhàn)(分布式事務(wù)一致性保證)》

《手把手0基礎(chǔ)項(xiàng)目實(shí)戰(zhàn)(五)——電商系統(tǒng)的緩存策略》

《手把手0基礎(chǔ)項(xiàng)目實(shí)戰(zhàn)(六)——基于配置中心實(shí)現(xiàn)集群配置的集中管理和熔斷機(jī)制》

《手把手0基礎(chǔ)項(xiàng)目實(shí)戰(zhàn)(七)——電商系統(tǒng)的日志監(jiān)控方案》

《手把手0基礎(chǔ)項(xiàng)目實(shí)戰(zhàn)(八)——基于JMeter的系統(tǒng)性能測(cè)試》

幾乎所有的Web系統(tǒng)都需要登錄、權(quán)限管理、角色管理等功能,而且這些功能往往具有較大的普適性,與系統(tǒng)具體的業(yè)務(wù)關(guān)聯(lián)性較小。因此,這些功能完全可以被封裝成一個(gè)可配置、可插拔的框架,當(dāng)開發(fā)一個(gè)新系統(tǒng)的時(shí)候直接將其引入、并作簡(jiǎn)單配置即可,無需再?gòu)念^開發(fā),極大節(jié)約了人力成本、時(shí)間成本。

在Java Web領(lǐng)域,有兩大主流的安全框架,Spring Security和Apache Shiro。他們都能實(shí)現(xiàn)用戶鑒權(quán)、權(quán)限管理、角色管理、防止Web攻擊等功能,而且這兩套開源框架都已經(jīng)過大量項(xiàng)目的驗(yàn)證,趨于穩(wěn)定成熟,可以很好地為我們的項(xiàng)目服務(wù)。

本文將帶領(lǐng)大家從頭開始實(shí)現(xiàn)一套安全框架,該框架與Spring Boot深度融合,從而能夠幫助大家加深對(duì)Spring Boot的理解。這套框架中將涉及到如下內(nèi)容:

Spring Boot AOP

Spring Boot 全局異常處理

Spring Boot CommandLineRunner

Java 反射機(jī)制

分布式系統(tǒng)中Session的集中式管理

本文將從安全框架的設(shè)計(jì)與實(shí)現(xiàn)兩個(gè)角度帶領(lǐng)大家完成安全框架的開發(fā),廢話不多說,現(xiàn)在開始吧~

項(xiàng)目完整源碼下載

https://github.com/bz51/Sprin...

1. 安全框架的設(shè)計(jì) 1.1 開發(fā)目標(biāo)

在所有事情開始之前,我們首先要搞清楚,我們究竟要實(shí)現(xiàn)哪些功能?

用戶登錄
所有系統(tǒng)都需要登錄功能,這毫無疑問,也不必多說。

角色管理
每個(gè)用戶都有且僅有一種角色,比如:系統(tǒng)管理員、普通用戶、企業(yè)用戶等等。管理員可以添加、刪除、查詢、修改角色信息。

權(quán)限管理
每種角色可以擁有不同的權(quán)限,管理員可以創(chuàng)建、修改、查詢、刪除權(quán)限,也可以為某一種角色添加、刪除權(quán)限。

權(quán)限檢測(cè)
用戶調(diào)用每一個(gè)接口,都需要校驗(yàn)該用戶是否具備調(diào)用該接口的權(quán)限。

當(dāng)我們明確了開發(fā)目標(biāo)之后,下面就需要基于這些目標(biāo),設(shè)計(jì)我們的系統(tǒng)。我們首先要做的就是要搞清楚“用戶”、“角色”、“權(quán)限”的定義以及他們之間的關(guān)系。這在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)中被稱為“領(lǐng)域模型”。

1.2 領(lǐng)域模型

權(quán)限:

權(quán)限表示某一用戶是否具有操作某一資源的能力。

權(quán)限一般用“資源名稱:操作名稱”來表示。比如:創(chuàng)建用戶的權(quán)限可以用“user:create”來表示,刪除用戶的權(quán)限可以用“user:delete”來表示。

在Web系統(tǒng)中,權(quán)限和接口呈一一對(duì)應(yīng)關(guān)系,比如:“user:create”對(duì)應(yīng)著創(chuàng)建用戶的接口,“user:delete”對(duì)應(yīng)著刪除用戶的接口。因此,權(quán)限也可以理解成一個(gè)用戶是否具備操作某一個(gè)接口的能力。

角色:

角色是一組權(quán)限的集合。角色規(guī)定了某一類用戶共同具備的權(quán)限集合。

比如:超級(jí)管理員這種角色擁有“user:create”、“user:delete”等權(quán)限,而普通用戶只有“user:create”權(quán)限。

從領(lǐng)域模型中可知,角色和權(quán)限之間呈多對(duì)多的聚合關(guān)系,即一種角色可以包含多個(gè)權(quán)限,一個(gè)權(quán)限也可以屬于多種角色,并且權(quán)限可以脫離于角色而多帶帶存在,因此他們之間是一種弱依賴關(guān)系——聚合關(guān)系。

用戶:

用戶和角色之間呈多對(duì)一的聚合關(guān)系,即一個(gè)用戶只能屬于一種角色,而一種角色卻可以包含多個(gè)用戶。并且角色可以脫離于用戶多帶帶存在,因此他們之間是一種弱依賴關(guān)系——聚合關(guān)系。

1.3 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)

當(dāng)我們捋清楚了“權(quán)限”、“用戶”、“角色”的定義和他們之間的關(guān)系后,下面我們就可以基于這個(gè)領(lǐng)域模型設(shè)計(jì)出具體的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)。

為了能夠方便地給每一個(gè)接口標(biāo)注權(quán)限,我們需要自定義三個(gè)注解@Login@Role@Permission

@Login:用于標(biāo)識(shí)當(dāng)前接口是否需要登錄。當(dāng)接口使用了這個(gè)注解后,用戶只有在登錄后才能訪問。

@Role("角色名"):用于標(biāo)識(shí)允許調(diào)用當(dāng)前接口的角色。當(dāng)接口使用了這個(gè)注解后,只有指定角色的用戶才能調(diào)用本接口。

@Permission("權(quán)限名"):用于標(biāo)識(shí)允許調(diào)用當(dāng)前接口的權(quán)限。當(dāng)接口使用了這個(gè)注解后,只有具備指定權(quán)限的用戶才能調(diào)用本接口。

1.4 接口權(quán)限信息初始化流程

要使得這個(gè)安全框架運(yùn)行起來,首先就需要在系統(tǒng)初始化完成前,初始化所有接口的權(quán)限、角色等信息,這個(gè)過程即為“接口權(quán)限信息初始化流程”;然后在系統(tǒng)運(yùn)行期間,如果有用戶請(qǐng)求接口,就可以根據(jù)這些權(quán)限信息判斷該用戶是否有權(quán)限訪問接口。

這一小節(jié)主要介紹接口權(quán)限信息初始化流程,不涉及任何實(shí)現(xiàn)細(xì)節(jié),實(shí)現(xiàn)的細(xì)節(jié)將在本文的實(shí)現(xiàn)部分介紹。

當(dāng)Spring完成上下文的初始化后,需要掃描本項(xiàng)目中所有Controller類;

再依次掃描Controller類中的所有方法,獲取方法上的@GetMapping@PostMapping、@PutMapping@DeleteMapping,通過這些注解獲取接口的URL、請(qǐng)求方式等信息;

同時(shí),獲取方法上的@Login@Role@Permission,通過這些注解,獲取該接口是否需要登錄、允許訪問的角色以及允許訪問的權(quán)限信息;

將每個(gè)接口的權(quán)限信息、URL、請(qǐng)求方式存儲(chǔ)在Redis中,供用戶調(diào)用接口是鑒權(quán)使用。

1.5 用戶鑒權(quán)流程

所有的用戶請(qǐng)求在被執(zhí)行前都會(huì)被系統(tǒng)攔截,從請(qǐng)求中獲取請(qǐng)求的URL和請(qǐng)求方式;

然后從Redis中查詢?cè)摻涌趯?duì)應(yīng)的權(quán)限信息;

若該接口需要登錄,并且當(dāng)前用戶尚未登錄,則直接拒絕;

若該接口需要登錄,并且擁有已經(jīng)登錄,那么需要從請(qǐng)求頭中解析出SessionID,并到Redis中查詢?cè)撚脩舻臋?quán)限信息,然后拿著用戶的權(quán)限信息、角色信息和該接口的權(quán)限信息、角色信息進(jìn)行比對(duì)。若通過鑒權(quán),則執(zhí)行該接口;若未通過鑒權(quán),則直接拒絕請(qǐng)求。

2. 安全框架的實(shí)現(xiàn) 2.1 注解的實(shí)現(xiàn)

本套安全框架一共定義了四個(gè)注解:@AuthScan、@Login、@Role、@Permission。

2.1.1 @AuthScan

該注解用來告訴安全框架,本項(xiàng)目中所有Controller類所在的包,從而能夠幫助安全框架快速找到Controller類,避免了所有類的掃描。

它有且僅有一個(gè)參數(shù),用來指定Controller所在的包:@AuthScan("com.gaoxi.controller")。它的代碼實(shí)現(xiàn)如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthScan {
    public String value();
}

注解顧名思義,它是用來在代碼中進(jìn)行標(biāo)注,它本身不承載任何邏輯,通過注解

@Retention
它解釋說明了這個(gè)注解的的存活時(shí)間。它的取值如下:

RetentionPolicy.SOURCE 注解只在源碼階段保留,在編譯器進(jìn)行編譯時(shí)它將被丟棄忽視。

RetentionPolicy.CLASS 注解只被保留到編譯進(jìn)行的時(shí)候,它并不會(huì)被加載到 JVM 中。

RetentionPolicy.RUNTIME 注解可以保留到程序運(yùn)行的時(shí)候,它會(huì)被加載進(jìn)入到 JVM 中,所以在程序運(yùn)行時(shí)可以獲取到它們。

@Documented
顧名思義,這個(gè)元注解肯定是和文檔有關(guān)。它的作用是能夠?qū)⒆⒔庵械脑匕?Javadoc 中去。

@Target
當(dāng)一個(gè)注解被 @Target 注解時(shí),這個(gè)注解就被限定了運(yùn)用的場(chǎng)景。

ElementType.ANNOTATION_TYPE:可以給一個(gè)注解進(jìn)行注解

ElementType.CONSTRUCTOR:可以給構(gòu)造方法進(jìn)行注解

ElementType.FIELD:可以給屬性進(jìn)行注解

ElementType.LOCAL_VARIABLE:可以給局部變量進(jìn)行注解

ElementType.METHOD:可以給方法進(jìn)行注解

ElementType.PACKAGE:可以給一個(gè)包進(jìn)行注解

ElementType.PARAMETER:可以給一個(gè)方法內(nèi)的參數(shù)進(jìn)行注解

ElementType.TYPE:可以給一個(gè)類型進(jìn)行注解,比如類、接口、枚舉

2.1.2 @Login

這個(gè)注解用于標(biāo)識(shí)指定接口是否需要登錄后才能訪問,它有一個(gè)默認(rèn)的boolean類型的值,用于表示是否需要登錄,其代碼如下:

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

    // 是否需要登錄(默認(rèn)為true)
    public boolean value() default true;

}
2.1.3 @Role

該注解用于指定允許訪問當(dāng)前接口的角色,其代碼如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Role {
    public String value();
}
2.1.4 @Permission

該注解用于指定允許訪問當(dāng)前接口的權(quán)限,其代碼如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Permission {
    public String value();
}
2.2 權(quán)限信息初始化過程

上文中提到,注解本身不含任何業(yè)務(wù)邏輯,它只是在代碼中起一個(gè)標(biāo)識(shí)的作用,那么怎么才能讓注解“活”起來?這就需要通過反射機(jī)制來獲取注解。

2.2.1 在接口上聲明權(quán)限信息

當(dāng)完成這些注解的定義后,接下來就需要使用他們,如下面代碼所示:

public interface ProductController {

    /**
     * 創(chuàng)建產(chǎn)品
     * @param prodInsertReq 產(chǎn)品詳情
     * @return 是否創(chuàng)建成功
     */
    @PostMapping("product")
    @Login
    @Permission("product:create")
    public Result createProduct(ProdInsertReq prodInsertReq);
    
}

ProductController是一個(gè)Controller類,它提供了處理產(chǎn)品的各種接口。簡(jiǎn)單起見,這里只列出了一個(gè)創(chuàng)建產(chǎn)品的接口。
@PostMapping是SpringMVC提供的注解,用于標(biāo)識(shí)該接口的訪問路徑和訪問方式。
@Login聲明了該接口需要登錄后才能訪問。
@Permission聲明了用戶只有擁有product:create權(quán)限才能訪問該接口。

2.2.2 初始化權(quán)限信息

當(dāng)系統(tǒng)初始化的時(shí)候,需要加載接口上的這些權(quán)限信息,存儲(chǔ)在Redis中。在系統(tǒng)運(yùn)行期間,當(dāng)有用戶請(qǐng)求接口的時(shí)候,系統(tǒng)會(huì)根據(jù)接口的權(quán)限信息判斷用戶是否有訪問接口的權(quán)限。權(quán)限信息初始化過程的代碼如下:

/**
 * @author 大閑人柴毛毛
 * @date 2017/11/1 上午10:04
 *
 * @description 初始化權(quán)限信息
 */
@AuthScan("com.gaoxi.controller")
@Component
public class InitAuth implements CommandLineRunner {
    @Override
    public void run(String... strings) throws Exception {
        // 加載接口訪問權(quán)限
        loadAccessAuth();
    }
    
    ……
}

上述代碼定義了一個(gè)InitAuth類,該類實(shí)現(xiàn)了CommandLineRunner接口,該接口中含有run()方法,當(dāng)Spring的上下文初始化完成后,就會(huì)調(diào)用run(),從而完成權(quán)限信息的初始化過程。

該類使用了@AuthScan("com.gaoxi.controller")注解,用于標(biāo)識(shí)當(dāng)前項(xiàng)目Controller類所在的包名,從而避免掃描所有類,一定程度上加速系統(tǒng)初始化的速度。

@Component注解會(huì)在Spring容器初始化完成后,創(chuàng)建本類的對(duì)象,并加入IoC容器中。

下面來看一下loadAccessAuth()方法的具體實(shí)現(xiàn):

    /**
     * 加載接口訪問權(quán)限
     */
    private void loadAccessAuth() throws IOException {
        // 獲取待掃描的包名
        AuthScan authScan = AnnotationUtil.getAnnotationValueByClass(this.getClass(), AuthScan.class);
        String pkgName = authScan.value();

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

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

            // 遍歷函數(shù)
            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("接口訪問權(quán)限已加載完畢!"+accessAuthMap);
    }

首先會(huì)讀取本類上的@AuthScan注解,并獲取注解中聲明了Controller類所在的包pkgName;

pkgName是一個(gè)字符串,因此需要使用Java反射機(jī)制將字符串解析成Class對(duì)象。其解析過程通過工具包ClassUtil.getClasses(pkgName)完成,具體解析過程這里就不做詳細(xì)介紹了,感興趣的同學(xué)可以參閱本項(xiàng)目源碼。

ClassUtil.getClasses(pkgName)解析之后,該包下的所有Controller類將會(huì)被解析成List>對(duì)象,然后遍歷所有的Class對(duì)象;

然后依次獲取每個(gè)Class對(duì)象中的Method對(duì)象,并依次遍歷Method對(duì)象,通過buildAccessAuthEntity(method)方法將一個(gè)個(gè)Method對(duì)象解析成AccessAuthEntity對(duì)象(具體解析過程在稍后介紹);

最后將AccessAuthEntity對(duì)象存儲(chǔ)在Redis中,供用戶訪問接口時(shí)使用。

這就是整個(gè)權(quán)限信息初始化的過程,下面詳細(xì)介紹buildAccessAuthEntity(method)方法的解析過程,它究竟是如何將一個(gè)Mehtod對(duì)象解析成AccessAuthEntity對(duì)象?并且AccessAuthEntity對(duì)象的結(jié)構(gòu)究竟是怎樣的?

首先來看一下AccessAuthEntity的數(shù)據(jù)結(jié)構(gòu):

/**
 * @author 大閑人柴毛毛
 * @date 2017/11/1 上午11:05
 * @description 接口訪問權(quán)限的實(shí)體類
 */
public class AccessAuthEntity implements Serializable {

    /** 請(qǐng)求 URL */
    private String url;

    /** 接口方法名 */
    private String methodName;

    /** HTTP 請(qǐng)求方式 */
    private HttpMethodEnum httpMethodEnum;

    /** 當(dāng)前接口是否需要登錄 */
    private boolean isLogin;

    /** 當(dāng)前接口的訪問權(quán)限 */
    private String permission;
    
    // setter/getter省略
}

AccessAuthEntity用于存儲(chǔ)一個(gè)接口的訪問路徑、訪問方式和權(quán)限信息。在系統(tǒng)初始化的時(shí)候,Controller類中的每個(gè)Mehtod對(duì)象都會(huì)被buildAccessAuthEntity()方法解析成AccessAuthEntity對(duì)象。buildAccessAuthEntity()方法的代碼如下所示:

/**
 * 構(gòu)造AccessAuthEntity對(duì)象
 * @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;
}

該方法首先會(huì)獲取當(dāng)前Method上的XXXMapping四個(gè)注解,通過解析這些注解能夠獲取到當(dāng)前接口的訪問路徑和請(qǐng)求方式,并將這兩者存儲(chǔ)在AccessAuthEntity對(duì)象中。

然后通過getLoginAndPermission方法,解析當(dāng)前Method對(duì)象中的@Login 和@Permission信息,其代碼如下所示:

/**
 * 獲取指定方法上的@Login的值和@Permission的值
 * @param method 目標(biāo)方法
 * @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完成,具體的解析過程這里就不再贅述,感興趣的同學(xué)請(qǐng)參閱項(xiàng)目源碼。

到此為止,接口的訪問路徑、請(qǐng)求方式、是否需要登錄、權(quán)限信息都已經(jīng)解析成一個(gè)個(gè)AccessAuthEntity對(duì)象,并以“請(qǐng)求方式+訪問路徑”作為key,存儲(chǔ)在Redis中。接口權(quán)限信息的初始化過程也就完成了!

2.2.3 用戶鑒權(quán)

當(dāng)用戶請(qǐng)求所有接口前,系統(tǒng)都應(yīng)該攔截這些請(qǐng)求,只有在權(quán)限校驗(yàn)通過的情況下才運(yùn)行調(diào)用接口,否則直接拒絕請(qǐng)求。

基于上述需求,我們需要給Controller中所有方法執(zhí)行前增加切面,并將用于權(quán)限校驗(yàn)的代碼織入到該切面中,從而在方法執(zhí)行前完成權(quán)限校驗(yàn)。下面就詳細(xì)介紹在SpringBoot中AOP的使用。

首先,我們需要在項(xiàng)目的pom中引入AOP的依賴:



    org.springframework.boot
    spring-boot-starter-aop

創(chuàng)建切面類:

在類上必須添加@Aspect注解,用于標(biāo)識(shí)當(dāng)前類是一個(gè)AOP切面類

該類也必須添加@Component注解,讓Spring初始化完成后創(chuàng)建本類的對(duì)象,并加入IoC容器中

然后需要使用@Pointcut注解定義切點(diǎn);切點(diǎn)描述了哪些類中的哪些方法需要織入權(quán)限校驗(yàn)代碼。我們這里將所有Controller類中的所有方法作為切點(diǎn)。

當(dāng)完成切點(diǎn)的定義后,我們需要使用@Before注解聲明切面織入的時(shí)機(jī);由于我們需要在方法執(zhí)行前攔截所有的請(qǐng)求,因此使用@Before注解。

當(dāng)完成上述設(shè)置之后,所有Controller類中的函數(shù)在被調(diào)用前,都會(huì)執(zhí)行權(quán)限校驗(yàn)代碼。權(quán)限校驗(yàn)的詳細(xì)過程在authentication()方法中完成。

/**
 * @author 大閑人柴毛毛
 * @date 2017/11/2 下午7:06
 *
 * @description 訪問權(quán)限處理類(所有請(qǐng)求都要經(jīng)過此類)
 */
@Aspect
@Component
public class AccessAuthHandle {
    /** 定義切點(diǎn) */
    @Pointcut("execution(public * com.gaoxi.controller..*.*(..))")
    public void accessAuth(){}


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

        // 訪問鑒權(quán)
        authentication();

    }
}

權(quán)限校驗(yàn)過程

該方法首先會(huì)獲取當(dāng)前請(qǐng)求的訪問路徑和請(qǐng)求方法;

然后獲取HTTP請(qǐng)求頭中的SessionID,并從Redis中獲取該SessionID對(duì)應(yīng)的用戶信息;

然后根據(jù)接口訪問路徑和訪問方法,從Redis中獲取該接口的權(quán)限信息;到此為止,權(quán)限校驗(yàn)前的準(zhǔn)備工作都已完成,下面就要進(jìn)入權(quán)限校驗(yàn)過程了;

/**
 * 檢查當(dāng)前用戶是否允許訪問該接口
 */
private void authentication() {
    // 獲取 HttpServletRequest
    HttpServletRequest request = getHttpServletRequest();

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

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

    // 獲取SessionID對(duì)應(yīng)的用戶信息
    UserEntity userEntity = getUserEntity(sessionID);

    // 獲取接口權(quán)限信息
    AccessAuthEntity accessAuthEntity = getAccessAuthEntity(method, url);

    // 檢查權(quán)限
    authentication(userEntity, accessAuthEntity);
}

authentication():

首先判斷當(dāng)前接口是否需要登錄后才允許訪問,如果無需登錄,那么直接允許訪問;

若當(dāng)前接口需要登錄后才能訪問,那么判斷當(dāng)前用戶是否已經(jīng)登錄;若尚未登錄,則直接拒絕請(qǐng)求(通過拋出throw new CommonBizException(ExpCodeEnum.NO_PERMISSION)異常來拒絕請(qǐng)求,這由SpringBoot統(tǒng)一異常處理機(jī)制來完成,稍后會(huì)詳細(xì)介紹);若已經(jīng)登錄,則開始檢查權(quán)限信息;

權(quán)限檢查由checkPermission()方法完成,它會(huì)將用戶所具備的權(quán)限和接口要求的權(quán)限進(jìn)行比對(duì);如果用戶所具備的權(quán)限包含接口要求的權(quán)限,那么權(quán)限校驗(yàn)通過;反之,則通過拋異常的方式拒絕請(qǐng)求。

/**
 * 檢查權(quán)限
 * @param userEntity 當(dāng)前用戶的信息
 * @param accessAuthEntity 當(dāng)前接口的訪問權(quán)限
 */
private void authentication(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
    // 無需登錄
    if (!accessAuthEntity.isLogin()) {
        return;
    }

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

    // 檢查是否擁有權(quán)限
    checkPermission(userEntity, accessAuthEntity);
}

/**
 * 檢查當(dāng)前用戶是否擁有訪問該接口的權(quán)限
 * @param userEntity 用戶信息
 * @param accessAuthEntity 接口權(quán)限信息
 */
private void checkPermission(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
    // 獲取接口權(quán)限
    String accessPermission = accessAuthEntity.getPermission();

    // 獲取用戶權(quán)限
    List userPermissionList = userEntity.getRoleEntity().getPermissionList();

    // 判斷用戶是否包含接口權(quán)限
    if (CollectionUtils.isNotEmpty(userPermissionList)) {
        for (PermissionEntity permissionEntity : userPermissionList) {
            if (permissionEntity.getPermission().equals(accessPermission)) {
                return;
            }
        }
    }

    // 沒有權(quán)限
    throw new CommonBizException(ExpCodeEnum.NO_PERMISSION);
}


/**
 * 檢查當(dāng)前接口是否需要登錄
 * @param userEntity 用戶信息
 * @param accessAuthEntity 接口訪問權(quán)限
 */
private void checkLogin(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
    // 尚未登錄
    if (accessAuthEntity.isLogin() && userEntity==null) {
        throw new CommonBizException(ExpCodeEnum.UNLOGIN);
    }
}

全局異常處理
為了是得代碼具備良好的可讀性,這里使用了SpringBoot提供的全局異常處理機(jī)制。我們只需拋出異常即可,這些異常會(huì)被我們預(yù)先設(shè)置的全局異常處理類捕獲并處理。全局異常處理本質(zhì)上借助于AOP完成。

我們需要定義全局異常處理類,它只是一個(gè)普通類,我們只要用@ControllerAdvice注解聲明即可

我們還需要在這個(gè)類上增加@ResponseBody注解,它能夠幫助我們當(dāng)處理完異常后,直接向用戶返回JSON格式的錯(cuò)誤信息,而無需我們手動(dòng)處理。

在這個(gè)類中,我們根據(jù)異常類型不同,定義了兩個(gè)異常處理函數(shù),分別用于捕獲業(yè)務(wù)異常、系統(tǒng)異常。并且需要使用@ExceptionHandler注解告訴Spring,該方法用于處理什么類型的異常。

當(dāng)我們完成上述配置后,只要項(xiàng)目中任何地方拋出異常,都會(huì)被這個(gè)全局異常處理類捕獲,并根據(jù)拋出異常的類型選擇相應(yīng)的異常處理函數(shù)。

/**
 * @Author 大閑人柴毛毛
 * @Date 2017/10/27 下午11:02
 * REST接口的通用異常處理
 */
@ControllerAdvice
@ResponseBody
public class ExceptionHandle {

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

    /**
     * 業(yè)務(wù)異常處理
     * @param exception
     * @param 
     * @return
     */
    @ExceptionHandler(CommonBizException.class)
    public  Result exceptionHandler(CommonBizException exception) {
        return Result.newFailureResult(exception);
    }

    /**
     * 系統(tǒng)異常處理
     * @param exception
     * @return
     */
    @ExceptionHandler(Exception.class)
    public  Result sysExpHandler(Exception exception) {
        logger.error("系統(tǒng)異常 ",exception);
        return Result.newFailureResult();
    }
}

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/68807.html

相關(guān)文章

  • 架構(gòu) - 收藏集 - 掘金

    摘要:淺談秒殺系統(tǒng)架構(gòu)設(shè)計(jì)后端掘金秒殺是電子商務(wù)網(wǎng)站常見的一種營(yíng)銷手段。這兩個(gè)項(xiàng)目白話網(wǎng)站架構(gòu)演進(jìn)后端掘金這是白話系列的文章。 淺談秒殺系統(tǒng)架構(gòu)設(shè)計(jì) - 后端 - 掘金秒殺是電子商務(wù)網(wǎng)站常見的一種營(yíng)銷手段。 不要整個(gè)系統(tǒng)宕機(jī)。 即使系統(tǒng)故障,也不要將錯(cuò)誤數(shù)據(jù)展示出來。 盡量保持公平公正。 實(shí)現(xiàn)效果 秒殺開始前,搶購(gòu)按鈕為活動(dòng)未開始。 秒殺開始時(shí),搶購(gòu)按鈕可以點(diǎn)擊下單。 秒殺結(jié)束后,按鈕按鈕變...

    Riddler 評(píng)論0 收藏0
  • 把手教你電商爬蟲-第實(shí)戰(zhàn)尚妝網(wǎng)AJAX請(qǐng)求處理和內(nèi)容提取

    摘要:系列教程手把手教你寫電商爬蟲第一課找個(gè)軟柿子捏捏手把手教你寫電商爬蟲第二課實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲看完兩篇,相信大家已經(jīng)從開始的小菜鳥晉升為中級(jí)菜鳥了,好了,那我們就繼續(xù)我們的爬蟲課程。 系列教程: 手把手教你寫電商爬蟲-第一課 找個(gè)軟柿子捏捏手把手教你寫電商爬蟲-第二課 實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲 看完兩篇,相信大家已經(jīng)從開始的小菜鳥晉升為中級(jí)菜鳥了,好了,那我們就繼續(xù)我們的爬蟲課...

    widuu 評(píng)論0 收藏0
  • 把手教你電商爬蟲-第實(shí)戰(zhàn)尚妝網(wǎng)AJAX請(qǐng)求處理和內(nèi)容提取

    摘要:系列教程手把手教你寫電商爬蟲第一課找個(gè)軟柿子捏捏手把手教你寫電商爬蟲第二課實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲看完兩篇,相信大家已經(jīng)從開始的小菜鳥晉升為中級(jí)菜鳥了,好了,那我們就繼續(xù)我們的爬蟲課程。 系列教程: 手把手教你寫電商爬蟲-第一課 找個(gè)軟柿子捏捏手把手教你寫電商爬蟲-第二課 實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲 看完兩篇,相信大家已經(jīng)從開始的小菜鳥晉升為中級(jí)菜鳥了,好了,那我們就繼續(xù)我們的爬蟲課...

    GraphQuery 評(píng)論0 收藏0
  • 把手教你電商爬蟲-第二課 實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲

    摘要:剩下的同學(xué),我們繼續(xù)了可以看出,作為一個(gè)完善的電商網(wǎng)站,尚妝網(wǎng)有著普通電商網(wǎng)站所擁有的主要的元素,包括分類,分頁(yè),主題等等。 系列教程 手把手教你寫電商爬蟲-第一課 找個(gè)軟柿子捏捏 如果沒有看過第一課的朋友,請(qǐng)先移步第一課,第一課講了一些基礎(chǔ)性的東西,通過軟柿子切糕王子這個(gè)電商網(wǎng)站好好的練了一次手,相信大家都應(yīng)該對(duì)寫爬蟲的流程有了一個(gè)大概的了解,那么這課咱們就話不多說,正式上戰(zhàn)場(chǎng),對(duì)壘...

    junfeng777 評(píng)論0 收藏0
  • 把手教你電商爬蟲-第二課 實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲

    摘要:剩下的同學(xué),我們繼續(xù)了可以看出,作為一個(gè)完善的電商網(wǎng)站,尚妝網(wǎng)有著普通電商網(wǎng)站所擁有的主要的元素,包括分類,分頁(yè),主題等等。 系列教程 手把手教你寫電商爬蟲-第一課 找個(gè)軟柿子捏捏 如果沒有看過第一課的朋友,請(qǐng)先移步第一課,第一課講了一些基礎(chǔ)性的東西,通過軟柿子切糕王子這個(gè)電商網(wǎng)站好好的練了一次手,相信大家都應(yīng)該對(duì)寫爬蟲的流程有了一個(gè)大概的了解,那么這課咱們就話不多說,正式上戰(zhàn)場(chǎng),對(duì)壘...

    objc94 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<