天天看點

Spring整合Shiro使用EL表達式

首先需要在你的工程中加入shiro-spring-xxx.jar,如果是使用Maven管理你的工程,則可以在你的依賴中加入以下依賴,筆者這裡是選擇的目前最新的1.4.0版本。

接下來需要在你的web.xml中定義一個shiroFilter,應用它來攔截所有的需要權限控制的請求,通常是配置為<code>/*</code>。另外該Filter需要加入最前面,以確定請求進來後最先通過shiro的權限控制。這裡的Filter對應的class配置的是DelegatingFilterProxy,這是Spring提供的一個Filter的代理,可以使用Spring bean容器中的一個bean來作為目前的Filter執行個體,對應的bean就會取<code>filter-name</code>對應的那個bean。是以下面的配置會到bean容器中尋找一個名為shiroFilter的bean。

獨立使用Shiro時通常會定義一個<code>org.apache.shiro.web.servlet.ShiroFilter</code>來做類似的事。

接下來就是在bean容器中定義我們的shiroFilter了。如下我們定義了一個ShiroFilterFactoryBean,其會産生一個AbstractShiroFilter類型的bean。通過ShiroFilterFactoryBean我們可以指定一個SecurityManager,這裡使用的DefaultWebSecurityManager需要指定一個Realm,如果需要指定多個Realm則通過realms指定。這裡簡單起見就直接使用基于文本定義的TextConfigurationRealm。通過loginUrl指定登入位址、successUrl指定登入成功後需要跳轉的位址,unauthorizedUrl指定權限不足時的提示頁面。filterChainDefinitions則定義URL與需要使用的Filter之間的關系,等号右邊的是filter的别名,預設的别名都定義在<code>org.apache.shiro.web.filter.mgt.DefaultFilter</code>這個枚舉類中。

如果需要在filterChainDefinitions定義中使用自定義的Filter,則可以通過ShiroFilterFactoryBean的filters指定自定義的Filter及其别名映射關系。比如下面這樣我們新增了一個别名為logger的Filter,并在filterChainDefinitions中指定了<code>/**</code>需要應用别名為logger的Filter。

其實我們需要應用的Filter别名定義也可以不直接通過ShiroFilterFactoryBean的setFilters()來指定,而是直接在對應的bean容器中定義對應的Filter對應的bean。因為預設情況下,ShiroFilterFactoryBean會把bean容器中的所有的Filter類型的bean以其id為别名注冊到filters中。是以上面的定義等價于下面這樣。

經過以上幾步,Shiro和Spring的整合就完成了,這個時候我們請求工程的任意路徑都會要求我們登入,且會自動跳轉到<code>loginUrl</code>指定的路徑讓我們輸入使用者名/密碼登入。這個時候我們應該提供一個表單,通過username獲得使用者名,通過password獲得密碼,然後送出登入請求的時候請求需要送出到<code>loginUrl</code>指定的位址,但是請求方式需要變為POST。登入時使用的使用者名/密碼是我們在TextConfigurationRealm中定義的使用者名/密碼,基于我們上面的配置則可以使用user1/pass1、admin/admin等。登入成功後就會跳轉到<code>successUrl</code>參數指定的位址了。如果我們是使用user1/pass1登入的,則我們還可以試着通路一下<code>/admin/index</code>,這個時候會因為權限不足跳轉到<code>unauthorized.jsp</code>。

Shiro提供的權限控制注解如下:

RequiresAuthentication:需要使用者在目前會話中是被認證過的,即需要通過使用者名/密碼登入過,不包括RememberMe自動登入。

RequiresUser:需要使用者是被認證過的,可以是在本次會話中通過使用者名/密碼登入認證,也可以是通過RememberMe自動登入。

RequiresGuest:需要使用者是未登入的。

RequiresRoles:需要使用者擁有指定的角色。

RequiresPermissions:需要使用者擁有指定的權限。

前面三個都很好了解,而後面兩個是類似的。筆者這裡拿@RequiresPermissions來做個示例。首先我們把上面定義的Realm改一下,給role添權重限。這樣我們的user1将擁有perm1、perm2和perm3的權限,而user2将擁有perm1、perm3和perm4的權限。

@RequiresPermissions可以添加在方法上,用來指定調用該方法時需要擁有的權限。下面的代碼我們就指定了在通路<code>/perm1</code>時必須擁有<code>perm1</code>這個權限。這個時候user1和user2都能通路。

如果需要指定必須同時擁有多個權限才能通路某個方法,可以把需要指定的權限以數組的形式指定(注解上的數組屬性指定單個的時候可以不加大括号,需要指定多個時就需要加大括号)。比如下面這樣我們就指定了在通路<code>/perm1AndPerm4</code>時使用者必須同時擁有<code>perm1</code>和<code>perm4</code>這兩個權限。這時候就隻有user2可以通路,因為隻有它才同時擁有<code>perm1</code>和<code>perm4</code>。

當同時指定了多個權限時,預設多個權限之間的關系是與的關系,即需要同時擁有指定的所有的權限。如果隻需要擁有指定的多個權限中的一個就可以通路,則我們可以通過<code>logical=Logical.OR</code>指定多個權限之間是或的關系。比如下面這樣我們就指定了在通路<code>/perm1OrPerm4</code>時隻需要擁有<code>perm1</code>或<code>perm4</code>權限即可,這樣user1和user2都可以通路該方法。

@RequiresPermissions也可以标注在Class上,表示在外部通路Class中的方法時都需要有對應的權限。比如下面這樣我們在Class級别指定了需要擁有權限<code>perm2</code>,而在<code>index()</code>方法上則沒有指定需要任何權限,但是我們在通路該方法時還是需要擁有Class級别指定的權限。此時将隻有user1可以通路。

當Class和方法級别都同時擁有@RequiresPermissions時,方法級别的擁有更高的優先級,而且此時将隻會校驗方法級别要求的權限。如下我們在Class級别指定了需要<code>perm2</code>權限,而在方法級别指定了需要<code>perm3</code>權限,那麼在通路<code>/foo</code>時将隻需要擁有<code>perm3</code>權限即可通路到<code>index()</code>方法。是以此時user1和user2都可以通路<code>/foo</code>。

但是如果此時我們在Class上新增<code>@RequiresRoles("role1")</code>指定需要擁有角色role1,那麼此時通路<code>/foo</code>時需要擁有Class上的role1和<code>index()</code>方法上<code>@RequiresPermissions("perm3")</code>指定的<code>perm3</code>權限。因為<code>RequiresRoles</code>和<code>RequiresPermissions</code>屬于不同次元的權限定義,Shiro在校驗的時候都将校驗一遍,但是如果Class和方法上都擁有同類型的權限控制定義的注解時,則隻會以方法上的定義為準。

雖然示例中使用的隻是<code>RequiresPermissions</code>,但是其它權限控制注解的用法也是類似的,其它注解的用法請感興趣的朋友自己實踐。

上面使用<code>@RequiresPermissions</code>我們指定的權限都是靜态的,寫本文的一個主要目的是介紹一種方法,通過擴充實作來使指定的權限可以是動态的。但是在擴充前我們得知道它底層的工作方式,即實作原理,我們才能進行擴充。是以接下來我們先來看一下Shiro整合Spring後使用<code>@RequiresPermissions</code>的工作原理。在啟用對<code>@RequiresPermissions</code>的支援時我們定義了如下bean,這是一個Advisor,其繼承自StaticMethodMatcherPointcutAdvisor,它的方法比對邏輯是隻要Class或Method上擁有Shiro的幾個權限控制注解即可,而攔截以後的處理邏輯則是由相應的Advice指定。

以下是AuthorizationAttributeSourceAdvisor的源碼。我們可以看到在其構造方法中通過<code>setAdvice()</code>指定了AopAllianceAnnotationsAuthorizingMethodInterceptor這個Advice實作類,這是基于MethodInterceptor的實作。

AopAllianceAnnotationsAuthorizingMethodInterceptor的源碼如下。其實作的MethodInterceptor接口的invoke方法又調用了父類的invoke方法。同時我們要看到在其構造方法中建立了一些AuthorizingAnnotationMethodInterceptor實作,這些實作才是實作權限控制的核心,待會我們會挑出PermissionAnnotationMethodInterceptor實作類來看其具體的實作邏輯。

通過看父類的invoke方法實作,最終我們會看到核心邏輯是調用assertAuthorized方法,而該方法的實作(源碼如下)又是依次判斷配置的AuthorizingAnnotationMethodInterceptor是否支援目前方法進行權限校驗(通過判斷Class或Method上是否擁有其支援的注解),當支援時則會調用其assertAuthorized方法進行權限校驗,而AuthorizingAnnotationMethodInterceptor又會調用AuthorizingAnnotationHandler的assertAuthorized方法。

接下來我們再回過頭來看AopAllianceAnnotationsAuthorizingMethodInterceptor的定義的PermissionAnnotationMethodInterceptor,其源碼如下。結合AopAllianceAnnotationsAuthorizingMethodInterceptor的源碼和PermissionAnnotationMethodInterceptor的源碼,我們可以看到PermissionAnnotationMethodInterceptor中這時候指定了PermissionAnnotationHandler和SpringAnnotationResolver。PermissionAnnotationHandler是AuthorizingAnnotationHandler的一個子類。是以我們最終的權限控制由PermissionAnnotationHandler的assertAuthorized實作決定。

接下來我們來看PermissionAnnotationHandler的assertAuthorized方法實作,其完整代碼如下。從實作上我們可以看到其會從Annotation中擷取配置的權限值,而這裡的Annotation就是RequiresPermissions注解。而且在進行權限校驗時都是直接使用的我們定義注解時指定的文本值,待會我們進行擴充時就将從這裡入手。

通過前面的介紹我們知道PermissionAnnotationHandler的assertAuthorized方法參數的Annotation是由AuthorizingAnnotationMethodInterceptor在調用AuthorizingAnnotationHandler的assertAuthorized方法時傳遞的。其源碼如下,從源碼中我們可以看到Annotation是通過getAnnotation方法獲得的。

沿着這個方向走下去,最終我們會找到SpringAnnotationResolver的getAnnotation方法實作,其實作如下。從下面的代碼可以看到,其在尋找注解時是優先尋找Method上的,如果在Method上沒有找到會從目前方法調用的所屬Class上尋找對應的注解。從這裡也可以看到為什麼我們之前在Class和Method上都定義了相同類型的權限控制注解時生效的是Method上的,而單獨存在的時候就是單獨定義的那個生效了。

通過以上的源碼閱讀,相信讀者對于Shiro整合Spring後支援的權限控制注解的原理已經有了比較深入的了解。上面貼出的源碼隻是部分筆者認為比較核心的,有想詳細了解完整内容的請讀者自己沿着筆者提到的思路去閱讀完整代碼。 了解了這塊基于注解進行權限控制的原理後,讀者朋友們也可以根據實際的業務需要進行相應的擴充。

假設現在内部有下面這樣一個接口,其中有一個query方法,接收一個參數type。這裡我們簡化一點,假設隻要接收這麼一個參數,然後對應不同的取值時将傳回不同的結果。

這個接口是對外開放的,通過對應的URL可以請求到該方法,我們定義了對應的Controller方法如下:

上面的接口服務在進行查詢的時候針對type是有權限的,不是每個使用者都可以使用每種type進行查詢的,需要擁有對應的權限才行。是以針對上面的處理器方法我們需要加上權限控制,而且在控制時需要的權限是随着參數type動态變的。假設關于type的每項權限的定義是query:type的形式,比如type=1時需要的權限是query:1,type=2時需要的權限是query:2。在沒有與Spring整合時,我們會如下這樣做:

但是與Spring整合後,上面的做法耦合性強,我們會更希望通過整合後的注解來進行權限控制。對于上面的場景我們更希望通過<code>@RequiresPermissions</code>來指定需要的權限,但是<code>@RequiresPermissions</code>中定義的權限是靜态文本,固定的。它沒法滿足我們動态的需求。這個時候可能你會想着我們可以把Controller處理方法拆分為多個,單獨進行權限控制。比如下面這樣:

定義了自己的PermissionAnnotationMethodInterceptor後,我們需要替換原來的PermissionAnnotationMethodInterceptor為我們自己的PermissionAnnotationMethodInterceptor。根據前面介紹的Shiro整合Spring後使用<code>@RequiresPermissions</code>等注解的原理我們知道PermissionAnnotationMethodInterceptor是由AopAllianceAnnotationsAuthorizingMethodInterceptor指定的,而後者又是由AuthorizationAttributeSourceAdvisor指定的。為此我們需要在定義AuthorizationAttributeSourceAdvisor時通過顯示定義AopAllianceAnnotationsAuthorizingMethodInterceptor的方式顯示的定義其中的AuthorizingAnnotationMethodInterceptor,然後把自帶的PermissionAnnotationMethodInterceptor替換為我們自定義的SelfAuthorizingAnnotationMethodInterceptor。替換後的定義如下:

為了示範前面示例的動态的權限,我們把角色與權限的關系調整如下,讓role1、role2和role3分别擁有query:1、query:2和query:3的權限。此時user1将擁有query:1和query:2的權限。

此時<code>@RequiresPermissions</code>中指定權限時就可以使用Spring EL表達式支援的文法了。因為我們在定義SelfPermissionAnnotationMethodInterceptor時已經指定了應用基于模闆的表達式解析,此時權限中定義的文本都将作為文本解析,動态的部分預設需要使用<code>#{</code>字首和<code>}</code>字尾包起來(這個字首和字尾是可以指定的,但是預設就好)。在動态部分中可以使用<code>#</code>字首引用變量,基于方法的表達式解析中可以使用參數名或<code>p參數索引</code>的形式引用方法參數。是以上面我們需要動态的權限的query方法的<code>@RequiresPermissions</code>定義如下。

這樣user1在通路<code>/service/1</code>和<code>/service/2</code>是OK的,但是在通路<code>/service/3</code>和<code>/service/300</code>時會提示沒有權限,因為user1沒有<code>query:3</code>和<code>query:300</code>的權限。