摘要:在領(lǐng)域,有兩大主流的安全框架,和。角色角色是一組權(quán)限的集合。安全框架的實(shí)現(xiàn)注解的實(shí)現(xiàn)本套安全框架一共定義了四個(gè)注解。該注解用來(lái)告訴安全框架,本項(xiàng)目中所有類(lèi)所在的包,從而能夠幫助安全框架快速找到類(lèi),避免了所有類(lèi)的掃描。
寫(xiě)在最前
本文是《手把手項(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)(三)——教你開(kāi)發(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)開(kāi)發(fā)一個(gè)新系統(tǒng)的時(shí)候直接將其引入、并作簡(jiǎn)單配置即可,無(wú)需再?gòu)念^開(kāi)發(fā),極大節(jié)約了人力成本、時(shí)間成本。
在Java Web領(lǐng)域,有兩大主流的安全框架,Spring Security和Apache Shiro。他們都能實(shí)現(xiàn)用戶(hù)鑒權(quán)、權(quán)限管理、角色管理、防止Web攻擊等功能,而且這兩套開(kāi)源框架都已經(jīng)過(guò)大量項(xiàng)目的驗(yàn)證,趨于穩(wěn)定成熟,可以很好地為我們的項(xiàng)目服務(wù)。
本文將帶領(lǐng)大家從頭開(kāi)始實(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)大家完成安全框架的開(kāi)發(fā),廢話(huà)不多說(shuō),現(xiàn)在開(kāi)始吧~
項(xiàng)目完整源碼下載https://github.com/bz51/Sprin...
1. 安全框架的設(shè)計(jì) 1.1 開(kāi)發(fā)目標(biāo)在所有事情開(kāi)始之前,我們首先要搞清楚,我們究竟要實(shí)現(xiàn)哪些功能?
用戶(hù)登錄
所有系統(tǒng)都需要登錄功能,這毫無(wú)疑問(wèn),也不必多說(shuō)。
角色管理
每個(gè)用戶(hù)都有且僅有一種角色,比如:系統(tǒng)管理員、普通用戶(hù)、企業(yè)用戶(hù)等等。管理員可以添加、刪除、查詢(xún)、修改角色信息。
權(quán)限管理
每種角色可以擁有不同的權(quán)限,管理員可以創(chuàng)建、修改、查詢(xún)、刪除權(quán)限,也可以為某一種角色添加、刪除權(quán)限。
權(quán)限檢測(cè)
用戶(hù)調(diào)用每一個(gè)接口,都需要校驗(yàn)該用戶(hù)是否具備調(diào)用該接口的權(quán)限。
當(dāng)我們明確了開(kāi)發(fā)目標(biāo)之后,下面就需要基于這些目標(biāo),設(shè)計(jì)我們的系統(tǒng)。我們首先要做的就是要搞清楚“用戶(hù)”、“角色”、“權(quán)限”的定義以及他們之間的關(guān)系。這在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)中被稱(chēng)為“領(lǐng)域模型”。
1.2 領(lǐng)域模型
權(quán)限:
權(quán)限表示某一用戶(hù)是否具有操作某一資源的能力。
權(quán)限一般用“資源名稱(chēng):操作名稱(chēng)”來(lái)表示。比如:創(chuàng)建用戶(hù)的權(quán)限可以用“user:create”來(lái)表示,刪除用戶(hù)的權(quán)限可以用“user:delete”來(lái)表示。
在Web系統(tǒng)中,權(quán)限和接口呈一一對(duì)應(yīng)關(guān)系,比如:“user:create”對(duì)應(yīng)著創(chuàng)建用戶(hù)的接口,“user:delete”對(duì)應(yīng)著刪除用戶(hù)的接口。因此,權(quán)限也可以理解成一個(gè)用戶(hù)是否具備操作某一個(gè)接口的能力。
角色:
角色是一組權(quán)限的集合。角色規(guī)定了某一類(lèi)用戶(hù)共同具備的權(quán)限集合。
比如:超級(jí)管理員這種角色擁有“user:create”、“user:delete”等權(quán)限,而普通用戶(hù)只有“user:create”權(quán)限。
從領(lǐng)域模型中可知,角色和權(quán)限之間呈多對(duì)多的聚合關(guān)系,即一種角色可以包含多個(gè)權(quán)限,一個(gè)權(quán)限也可以屬于多種角色,并且權(quán)限可以脫離于角色而多帶帶存在,因此他們之間是一種弱依賴(lài)關(guān)系——聚合關(guān)系。
用戶(hù):
用戶(hù)和角色之間呈多對(duì)一的聚合關(guān)系,即一個(gè)用戶(hù)只能屬于一種角色,而一種角色卻可以包含多個(gè)用戶(hù)。并且角色可以脫離于用戶(hù)多帶帶存在,因此他們之間是一種弱依賴(lài)關(guān)系——聚合關(guān)系。
1.3 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)當(dāng)我們捋清楚了“權(quán)限”、“用戶(hù)”、“角色”的定義和他們之間的關(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è)注解后,用戶(hù)只有在登錄后才能訪(fǎng)問(wèn)。
@Role("角色名"):用于標(biāo)識(shí)允許調(diào)用當(dāng)前接口的角色。當(dāng)接口使用了這個(gè)注解后,只有指定角色的用戶(hù)才能調(diào)用本接口。
@Permission("權(quán)限名"):用于標(biāo)識(shí)允許調(diào)用當(dāng)前接口的權(quán)限。當(dāng)接口使用了這個(gè)注解后,只有具備指定權(quán)限的用戶(hù)才能調(diào)用本接口。
1.4 接口權(quán)限信息初始化流程要使得這個(gè)安全框架運(yùn)行起來(lái),首先就需要在系統(tǒng)初始化完成前,初始化所有接口的權(quán)限、角色等信息,這個(gè)過(guò)程即為“接口權(quán)限信息初始化流程”;然后在系統(tǒng)運(yùn)行期間,如果有用戶(hù)請(qǐng)求接口,就可以根據(jù)這些權(quán)限信息判斷該用戶(hù)是否有權(quán)限訪(fǎng)問(wèn)接口。
這一小節(jié)主要介紹接口權(quán)限信息初始化流程,不涉及任何實(shí)現(xiàn)細(xì)節(jié),實(shí)現(xiàn)的細(xì)節(jié)將在本文的實(shí)現(xiàn)部分介紹。
當(dāng)Spring完成上下文的初始化后,需要掃描本項(xiàng)目中所有Controller類(lèi);
再依次掃描Controller類(lèi)中的所有方法,獲取方法上的@GetMapping、@PostMapping、@PutMapping和@DeleteMapping,通過(guò)這些注解獲取接口的URL、請(qǐng)求方式等信息;
同時(shí),獲取方法上的@Login、@Role和@Permission,通過(guò)這些注解,獲取該接口是否需要登錄、允許訪(fǎng)問(wèn)的角色以及允許訪(fǎng)問(wèn)的權(quán)限信息;
將每個(gè)接口的權(quán)限信息、URL、請(qǐng)求方式存儲(chǔ)在Redis中,供用戶(hù)調(diào)用接口是鑒權(quán)使用。
1.5 用戶(hù)鑒權(quán)流程所有的用戶(hù)請(qǐng)求在被執(zhí)行前都會(huì)被系統(tǒng)攔截,從請(qǐng)求中獲取請(qǐng)求的URL和請(qǐng)求方式;
然后從Redis中查詢(xún)?cè)摻涌趯?duì)應(yīng)的權(quán)限信息;
若該接口需要登錄,并且當(dāng)前用戶(hù)尚未登錄,則直接拒絕;
若該接口需要登錄,并且擁有已經(jīng)登錄,那么需要從請(qǐng)求頭中解析出SessionID,并到Redis中查詢(xún)?cè)撚脩?hù)的權(quán)限信息,然后拿著用戶(hù)的權(quán)限信息、角色信息和該接口的權(quán)限信息、角色信息進(jìn)行比對(duì)。若通過(guò)鑒權(quán),則執(zhí)行該接口;若未通過(guò)鑒權(quán),則直接拒絕請(qǐng)求。
2. 安全框架的實(shí)現(xiàn) 2.1 注解的實(shí)現(xiàn)本套安全框架一共定義了四個(gè)注解:@AuthScan、@Login、@Role、@Permission。
2.1.1 @AuthScan該注解用來(lái)告訴安全框架,本項(xiàng)目中所有Controller類(lèi)所在的包,從而能夠幫助安全框架快速找到Controller類(lèi),避免了所有類(lèi)的掃描。
它有且僅有一個(gè)參數(shù),用來(lái)指定Controller所在的包:@AuthScan("com.gaoxi.controller")。它的代碼實(shí)現(xiàn)如下:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AuthScan { public String value(); }
注解顧名思義,它是用來(lái)在代碼中進(jìn)行標(biāo)注,它本身不承載任何邏輯,通過(guò)注解
@Retention
它解釋說(shuō)明了這個(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è)類(lèi)型進(jìn)行注解,比如類(lèi)、接口、枚舉
2.1.2 @Login這個(gè)注解用于標(biāo)識(shí)指定接口是否需要登錄后才能訪(fǎng)問(wèn),它有一個(gè)默認(rèn)的boolean類(lèi)型的值,用于表示是否需要登錄,其代碼如下:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Login { // 是否需要登錄(默認(rèn)為true) public boolean value() default true; }2.1.3 @Role
該注解用于指定允許訪(fǎng)問(wèn)當(dāng)前接口的角色,其代碼如下:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Role { public String value(); }2.1.4 @Permission
該注解用于指定允許訪(fǎng)問(wèn)當(dāng)前接口的權(quán)限,其代碼如下:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Permission { public String value(); }2.2 權(quán)限信息初始化過(guò)程
上文中提到,注解本身不含任何業(yè)務(wù)邏輯,它只是在代碼中起一個(gè)標(biāo)識(shí)的作用,那么怎么才能讓注解“活”起來(lái)?這就需要通過(guò)反射機(jī)制來(lái)獲取注解。
2.2.1 在接口上聲明權(quán)限信息當(dāng)完成這些注解的定義后,接下來(lái)就需要使用他們,如下面代碼所示:
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類(lèi),它提供了處理產(chǎn)品的各種接口。簡(jiǎn)單起見(jiàn),這里只列出了一個(gè)創(chuàng)建產(chǎn)品的接口。
@PostMapping是SpringMVC提供的注解,用于標(biāo)識(shí)該接口的訪(fǎng)問(wèn)路徑和訪(fǎng)問(wèn)方式。
@Login聲明了該接口需要登錄后才能訪(fǎng)問(wèn)。
@Permission聲明了用戶(hù)只有擁有product:create權(quán)限才能訪(fǎng)問(wèn)該接口。
當(dāng)系統(tǒng)初始化的時(shí)候,需要加載接口上的這些權(quán)限信息,存儲(chǔ)在Redis中。在系統(tǒng)運(yùn)行期間,當(dāng)有用戶(hù)請(qǐng)求接口的時(shí)候,系統(tǒng)會(huì)根據(jù)接口的權(quán)限信息判斷用戶(hù)是否有訪(fǎng)問(wèn)接口的權(quán)限。權(quán)限信息初始化過(guò)程的代碼如下:
/** * @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 { // 加載接口訪(fǎng)問(wèn)權(quán)限 loadAccessAuth(); } …… }
上述代碼定義了一個(gè)InitAuth類(lèi),該類(lèi)實(shí)現(xiàn)了CommandLineRunner接口,該接口中含有run()方法,當(dāng)Spring的上下文初始化完成后,就會(huì)調(diào)用run(),從而完成權(quán)限信息的初始化過(guò)程。
該類(lèi)使用了@AuthScan("com.gaoxi.controller")注解,用于標(biāo)識(shí)當(dāng)前項(xiàng)目Controller類(lèi)所在的包名,從而避免掃描所有類(lèi),一定程度上加速系統(tǒng)初始化的速度。
@Component注解會(huì)在Spring容器初始化完成后,創(chuàng)建本類(lèi)的對(duì)象,并加入IoC容器中。
下面來(lái)看一下loadAccessAuth()方法的具體實(shí)現(xiàn):
/** * 加載接口訪(fǎng)問(wèn)權(quán)限 */ private void loadAccessAuth() throws IOException { // 獲取待掃描的包名 AuthScan authScan = AnnotationUtil.getAnnotationValueByClass(this.getClass(), AuthScan.class); String pkgName = authScan.value(); // 獲取包下所有類(lèi) List> classes = ClassUtil.getClasses(pkgName); if (CollectionUtils.isEmpty(classes)) { return; } // 遍歷類(lèi) 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("接口訪(fǎng)問(wèn)權(quán)限已加載完畢!"+accessAuthMap); }
首先會(huì)讀取本類(lèi)上的@AuthScan注解,并獲取注解中聲明了Controller類(lèi)所在的包pkgName;
pkgName是一個(gè)字符串,因此需要使用Java反射機(jī)制將字符串解析成Class對(duì)象。其解析過(guò)程通過(guò)工具包ClassUtil.getClasses(pkgName)完成,具體解析過(guò)程這里就不做詳細(xì)介紹了,感興趣的同學(xué)可以參閱本項(xiàng)目源碼。
ClassUtil.getClasses(pkgName)解析之后,該包下的所有Controller類(lèi)將會(huì)被解析成List
然后依次獲取每個(gè)Class對(duì)象中的Method對(duì)象,并依次遍歷Method對(duì)象,通過(guò)buildAccessAuthEntity(method)方法將一個(gè)個(gè)Method對(duì)象解析成AccessAuthEntity對(duì)象(具體解析過(guò)程在稍后介紹);
最后將AccessAuthEntity對(duì)象存儲(chǔ)在Redis中,供用戶(hù)訪(fǎng)問(wèn)接口時(shí)使用。
這就是整個(gè)權(quán)限信息初始化的過(guò)程,下面詳細(xì)介紹buildAccessAuthEntity(method)方法的解析過(guò)程,它究竟是如何將一個(gè)Mehtod對(duì)象解析成AccessAuthEntity對(duì)象?并且AccessAuthEntity對(duì)象的結(jié)構(gòu)究竟是怎樣的?
首先來(lái)看一下AccessAuthEntity的數(shù)據(jù)結(jié)構(gòu):
/** * @author 大閑人柴毛毛 * @date 2017/11/1 上午11:05 * @description 接口訪(fǎng)問(wèn)權(quán)限的實(shí)體類(lèi) */ 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)前接口的訪(fǎng)問(wèn)權(quán)限 */ private String permission; // setter/getter省略 }
AccessAuthEntity用于存儲(chǔ)一個(gè)接口的訪(fǎng)問(wèn)路徑、訪(fǎng)問(wèn)方式和權(quán)限信息。在系統(tǒng)初始化的時(shí)候,Controller類(lèi)中的每個(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è)注解,通過(guò)解析這些注解能夠獲取到當(dāng)前接口的訪(fǎng)問(wèn)路徑和請(qǐng)求方式,并將這兩者存儲(chǔ)在AccessAuthEntity對(duì)象中。
然后通過(guò)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; }
該注解的解析過(guò)程由注解工具包AnnotationUtil.getAnnotationValueByMethod完成,具體的解析過(guò)程這里就不再贅述,感興趣的同學(xué)請(qǐng)參閱項(xiàng)目源碼。
到此為止,接口的訪(fǎng)問(wèn)路徑、請(qǐng)求方式、是否需要登錄、權(quán)限信息都已經(jīng)解析成一個(gè)個(gè)AccessAuthEntity對(duì)象,并以“請(qǐng)求方式+訪(fǎng)問(wèn)路徑”作為key,存儲(chǔ)在Redis中。接口權(quán)限信息的初始化過(guò)程也就完成了!
2.2.3 用戶(hù)鑒權(quán)當(dāng)用戶(hù)請(qǐng)求所有接口前,系統(tǒng)都應(yīng)該攔截這些請(qǐng)求,只有在權(quán)限校驗(yàn)通過(guò)的情況下才運(yùn)行調(diào)用接口,否則直接拒絕請(qǐng)求。
基于上述需求,我們需要給Controller中所有方法執(zhí)行前增加切面,并將用于權(quán)限校驗(yàn)的代碼織入到該切面中,從而在方法執(zhí)行前完成權(quán)限校驗(yàn)。下面就詳細(xì)介紹在SpringBoot中AOP的使用。
首先,我們需要在項(xiàng)目的pom中引入AOP的依賴(lài):
org.springframework.boot spring-boot-starter-aop
創(chuàng)建切面類(lèi):
在類(lèi)上必須添加@Aspect注解,用于標(biāo)識(shí)當(dāng)前類(lèi)是一個(gè)AOP切面類(lèi)
該類(lèi)也必須添加@Component注解,讓Spring初始化完成后創(chuàng)建本類(lèi)的對(duì)象,并加入IoC容器中
然后需要使用@Pointcut注解定義切點(diǎn);切點(diǎn)描述了哪些類(lèi)中的哪些方法需要織入權(quán)限校驗(yàn)代碼。我們這里將所有Controller類(lèi)中的所有方法作為切點(diǎn)。
當(dāng)完成切點(diǎn)的定義后,我們需要使用@Before注解聲明切面織入的時(shí)機(jī);由于我們需要在方法執(zhí)行前攔截所有的請(qǐng)求,因此使用@Before注解。
當(dāng)完成上述設(shè)置之后,所有Controller類(lèi)中的函數(shù)在被調(diào)用前,都會(huì)執(zhí)行權(quán)限校驗(yàn)代碼。權(quán)限校驗(yàn)的詳細(xì)過(guò)程在authentication()方法中完成。
/** * @author 大閑人柴毛毛 * @date 2017/11/2 下午7:06 * * @description 訪(fǎng)問(wèn)權(quán)限處理類(lèi)(所有請(qǐng)求都要經(jīng)過(guò)此類(lèi)) */ @Aspect @Component public class AccessAuthHandle { /** 定義切點(diǎn) */ @Pointcut("execution(public * com.gaoxi.controller..*.*(..))") public void accessAuth(){} /** * 攔截所有請(qǐng)求 */ @Before("accessAuth()") public void doBefore() { // 訪(fǎng)問(wèn)鑒權(quán) authentication(); } }
權(quán)限校驗(yàn)過(guò)程
該方法首先會(huì)獲取當(dāng)前請(qǐng)求的訪(fǎng)問(wèn)路徑和請(qǐng)求方法;
然后獲取HTTP請(qǐng)求頭中的SessionID,并從Redis中獲取該SessionID對(duì)應(yīng)的用戶(hù)信息;
然后根據(jù)接口訪(fǎng)問(wèn)路徑和訪(fǎng)問(wèn)方法,從Redis中獲取該接口的權(quán)限信息;到此為止,權(quán)限校驗(yàn)前的準(zhǔn)備工作都已完成,下面就要進(jìn)入權(quán)限校驗(yàn)過(guò)程了;
/** * 檢查當(dāng)前用戶(hù)是否允許訪(fǎng)問(wèn)該接口 */ 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)的用戶(hù)信息 UserEntity userEntity = getUserEntity(sessionID); // 獲取接口權(quán)限信息 AccessAuthEntity accessAuthEntity = getAccessAuthEntity(method, url); // 檢查權(quán)限 authentication(userEntity, accessAuthEntity); }
authentication():
首先判斷當(dāng)前接口是否需要登錄后才允許訪(fǎng)問(wèn),如果無(wú)需登錄,那么直接允許訪(fǎng)問(wèn);
若當(dāng)前接口需要登錄后才能訪(fǎng)問(wèn),那么判斷當(dāng)前用戶(hù)是否已經(jīng)登錄;若尚未登錄,則直接拒絕請(qǐng)求(通過(guò)拋出throw new CommonBizException(ExpCodeEnum.NO_PERMISSION)異常來(lái)拒絕請(qǐng)求,這由SpringBoot統(tǒng)一異常處理機(jī)制來(lái)完成,稍后會(huì)詳細(xì)介紹);若已經(jīng)登錄,則開(kāi)始檢查權(quán)限信息;
權(quán)限檢查由checkPermission()方法完成,它會(huì)將用戶(hù)所具備的權(quán)限和接口要求的權(quán)限進(jìn)行比對(duì);如果用戶(hù)所具備的權(quán)限包含接口要求的權(quán)限,那么權(quán)限校驗(yàn)通過(guò);反之,則通過(guò)拋異常的方式拒絕請(qǐng)求。
/** * 檢查權(quán)限 * @param userEntity 當(dāng)前用戶(hù)的信息 * @param accessAuthEntity 當(dāng)前接口的訪(fǎng)問(wèn)權(quán)限 */ private void authentication(UserEntity userEntity, AccessAuthEntity accessAuthEntity) { // 無(wú)需登錄 if (!accessAuthEntity.isLogin()) { return; } // 檢查是否登錄 checkLogin(userEntity, accessAuthEntity); // 檢查是否擁有權(quán)限 checkPermission(userEntity, accessAuthEntity); } /** * 檢查當(dāng)前用戶(hù)是否擁有訪(fǎng)問(wèn)該接口的權(quán)限 * @param userEntity 用戶(hù)信息 * @param accessAuthEntity 接口權(quán)限信息 */ private void checkPermission(UserEntity userEntity, AccessAuthEntity accessAuthEntity) { // 獲取接口權(quán)限 String accessPermission = accessAuthEntity.getPermission(); // 獲取用戶(hù)權(quán)限 ListuserPermissionList = userEntity.getRoleEntity().getPermissionList(); // 判斷用戶(hù)是否包含接口權(quán)限 if (CollectionUtils.isNotEmpty(userPermissionList)) { for (PermissionEntity permissionEntity : userPermissionList) { if (permissionEntity.getPermission().equals(accessPermission)) { return; } } } // 沒(méi)有權(quán)限 throw new CommonBizException(ExpCodeEnum.NO_PERMISSION); } /** * 檢查當(dāng)前接口是否需要登錄 * @param userEntity 用戶(hù)信息 * @param accessAuthEntity 接口訪(fǎng)問(wèn)權(quán)限 */ private void checkLogin(UserEntity userEntity, AccessAuthEntity accessAuthEntity) { // 尚未登錄 if (accessAuthEntity.isLogin() && userEntity==null) { throw new CommonBizException(ExpCodeEnum.UNLOGIN); } }
全局異常處理
為了是得代碼具備良好的可讀性,這里使用了SpringBoot提供的全局異常處理機(jī)制。我們只需拋出異常即可,這些異常會(huì)被我們預(yù)先設(shè)置的全局異常處理類(lèi)捕獲并處理。全局異常處理本質(zhì)上借助于AOP完成。
我們需要定義全局異常處理類(lèi),它只是一個(gè)普通類(lèi),我們只要用@ControllerAdvice注解聲明即可
我們還需要在這個(gè)類(lèi)上增加@ResponseBody注解,它能夠幫助我們當(dāng)處理完異常后,直接向用戶(hù)返回JSON格式的錯(cuò)誤信息,而無(wú)需我們手動(dòng)處理。
在這個(gè)類(lèi)中,我們根據(jù)異常類(lèi)型不同,定義了兩個(gè)異常處理函數(shù),分別用于捕獲業(yè)務(wù)異常、系統(tǒng)異常。并且需要使用@ExceptionHandler注解告訴Spring,該方法用于處理什么類(lèi)型的異常。
當(dāng)我們完成上述配置后,只要項(xiàng)目中任何地方拋出異常,都會(huì)被這個(gè)全局異常處理類(lèi)捕獲,并根據(jù)拋出異常的類(lèi)型選擇相應(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://www.ezyhdfw.cn/yun/68807.html
摘要:淺談秒殺系統(tǒng)架構(gòu)設(shè)計(jì)后端掘金秒殺是電子商務(wù)網(wǎng)站常見(jiàn)的一種營(yíng)銷(xiāo)手段。這兩個(gè)項(xiàng)目白話(huà)網(wǎng)站架構(gòu)演進(jìn)后端掘金這是白話(huà)系列的文章。 淺談秒殺系統(tǒng)架構(gòu)設(shè)計(jì) - 后端 - 掘金秒殺是電子商務(wù)網(wǎng)站常見(jiàn)的一種營(yíng)銷(xiāo)手段。 不要整個(gè)系統(tǒng)宕機(jī)。 即使系統(tǒng)故障,也不要將錯(cuò)誤數(shù)據(jù)展示出來(lái)。 盡量保持公平公正。 實(shí)現(xiàn)效果 秒殺開(kāi)始前,搶購(gòu)按鈕為活動(dòng)未開(kāi)始。 秒殺開(kāi)始時(shí),搶購(gòu)按鈕可以點(diǎn)擊下單。 秒殺結(jié)束后,按鈕按鈕變...
摘要:系列教程手把手教你寫(xiě)電商爬蟲(chóng)第一課找個(gè)軟柿子捏捏手把手教你寫(xiě)電商爬蟲(chóng)第二課實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲(chóng)看完兩篇,相信大家已經(jīng)從開(kāi)始的小菜鳥(niǎo)晉升為中級(jí)菜鳥(niǎo)了,好了,那我們就繼續(xù)我們的爬蟲(chóng)課程。 系列教程: 手把手教你寫(xiě)電商爬蟲(chóng)-第一課 找個(gè)軟柿子捏捏手把手教你寫(xiě)電商爬蟲(chóng)-第二課 實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲(chóng) 看完兩篇,相信大家已經(jīng)從開(kāi)始的小菜鳥(niǎo)晉升為中級(jí)菜鳥(niǎo)了,好了,那我們就繼續(xù)我們的爬蟲(chóng)課...
摘要:系列教程手把手教你寫(xiě)電商爬蟲(chóng)第一課找個(gè)軟柿子捏捏手把手教你寫(xiě)電商爬蟲(chóng)第二課實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲(chóng)看完兩篇,相信大家已經(jīng)從開(kāi)始的小菜鳥(niǎo)晉升為中級(jí)菜鳥(niǎo)了,好了,那我們就繼續(xù)我們的爬蟲(chóng)課程。 系列教程: 手把手教你寫(xiě)電商爬蟲(chóng)-第一課 找個(gè)軟柿子捏捏手把手教你寫(xiě)電商爬蟲(chóng)-第二課 實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲(chóng) 看完兩篇,相信大家已經(jīng)從開(kāi)始的小菜鳥(niǎo)晉升為中級(jí)菜鳥(niǎo)了,好了,那我們就繼續(xù)我們的爬蟲(chóng)課...
摘要:剩下的同學(xué),我們繼續(xù)了可以看出,作為一個(gè)完善的電商網(wǎng)站,尚妝網(wǎng)有著普通電商網(wǎng)站所擁有的主要的元素,包括分類(lèi),分頁(yè),主題等等。 系列教程 手把手教你寫(xiě)電商爬蟲(chóng)-第一課 找個(gè)軟柿子捏捏 如果沒(méi)有看過(guò)第一課的朋友,請(qǐng)先移步第一課,第一課講了一些基礎(chǔ)性的東西,通過(guò)軟柿子切糕王子這個(gè)電商網(wǎng)站好好的練了一次手,相信大家都應(yīng)該對(duì)寫(xiě)爬蟲(chóng)的流程有了一個(gè)大概的了解,那么這課咱們就話(huà)不多說(shuō),正式上戰(zhàn)場(chǎng),對(duì)壘...
摘要:剩下的同學(xué),我們繼續(xù)了可以看出,作為一個(gè)完善的電商網(wǎng)站,尚妝網(wǎng)有著普通電商網(wǎng)站所擁有的主要的元素,包括分類(lèi),分頁(yè),主題等等。 系列教程 手把手教你寫(xiě)電商爬蟲(chóng)-第一課 找個(gè)軟柿子捏捏 如果沒(méi)有看過(guò)第一課的朋友,請(qǐng)先移步第一課,第一課講了一些基礎(chǔ)性的東西,通過(guò)軟柿子切糕王子這個(gè)電商網(wǎng)站好好的練了一次手,相信大家都應(yīng)該對(duì)寫(xiě)爬蟲(chóng)的流程有了一個(gè)大概的了解,那么這課咱們就話(huà)不多說(shuō),正式上戰(zhàn)場(chǎng),對(duì)壘...
閱讀 2000·2023-04-26 02:51
閱讀 3078·2021-09-10 10:50
閱讀 3358·2021-09-01 10:48
閱讀 3864·2019-08-30 15:53
閱讀 1998·2019-08-29 18:40
閱讀 544·2019-08-29 16:16
閱讀 2185·2019-08-29 13:21
閱讀 1946·2019-08-29 11:07