摘要:在領(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)限才能訪問該接口。
當(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
然后依次獲取每個(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)限 ListuserPermissionList = 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
摘要:淺談秒殺系統(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é)束后,按鈕按鈕變...
摘要:系列教程手把手教你寫電商爬蟲第一課找個(gè)軟柿子捏捏手把手教你寫電商爬蟲第二課實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲看完兩篇,相信大家已經(jīng)從開始的小菜鳥晉升為中級(jí)菜鳥了,好了,那我們就繼續(xù)我們的爬蟲課程。 系列教程: 手把手教你寫電商爬蟲-第一課 找個(gè)軟柿子捏捏手把手教你寫電商爬蟲-第二課 實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲 看完兩篇,相信大家已經(jīng)從開始的小菜鳥晉升為中級(jí)菜鳥了,好了,那我們就繼續(xù)我們的爬蟲課...
摘要:系列教程手把手教你寫電商爬蟲第一課找個(gè)軟柿子捏捏手把手教你寫電商爬蟲第二課實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲看完兩篇,相信大家已經(jīng)從開始的小菜鳥晉升為中級(jí)菜鳥了,好了,那我們就繼續(xù)我們的爬蟲課程。 系列教程: 手把手教你寫電商爬蟲-第一課 找個(gè)軟柿子捏捏手把手教你寫電商爬蟲-第二課 實(shí)戰(zhàn)尚妝網(wǎng)分頁(yè)商品采集爬蟲 看完兩篇,相信大家已經(jīng)從開始的小菜鳥晉升為中級(jí)菜鳥了,好了,那我們就繼續(xù)我們的爬蟲課...
摘要:剩下的同學(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ì)壘...
摘要:剩下的同學(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ì)壘...
閱讀 1835·2023-04-26 02:51
閱讀 2876·2021-09-10 10:50
閱讀 3077·2021-09-01 10:48
閱讀 3641·2019-08-30 15:53
閱讀 1829·2019-08-29 18:40
閱讀 417·2019-08-29 16:16
閱讀 2041·2019-08-29 13:21
閱讀 1828·2019-08-29 11:07