Spring 整合Shiro 并擴(kuò)展使用EL表達(dá)式的實(shí)例詳解
Shiro是一個(gè)輕量級(jí)的權(quán)限控制框架,應(yīng)用非常廣泛。本文的重點(diǎn)是介紹Spring整合Shiro,并通過(guò)擴(kuò)展使用Spring的EL表達(dá)式,使@RequiresRoles等支持動(dòng)態(tài)的參數(shù)。對(duì)Shiro的介紹則不在本文的討論范圍之內(nèi),讀者如果有對(duì)shiro不是很了解的,可以通過(guò)其官方網(wǎng)站了解相應(yīng)的信息。infoq上也有一篇文章對(duì)shiro介紹比較全面的,也是官方推薦的,其地址是https://www.infoq.com/articles/apache-shiro。
Shiro整合Spring
首先需要在你的工程中加入shiro-spring-xxx.jar,如果是使用Maven管理你的工程,則可以在你的依賴中加入以下依賴,筆者這里是選擇的當(dāng)前最新的1.4.0版本。
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
接下來(lái)需要在你的web.xml中定義一個(gè)shiroFilter,應(yīng)用它來(lái)攔截所有的需要權(quán)限控制的請(qǐng)求,通常是配置為/*。另外該Filter需要加入最前面,以確保請(qǐng)求進(jìn)來(lái)后最先通過(guò)shiro的權(quán)限控制。這里的Filter對(duì)應(yīng)的class配置的是DelegatingFilterProxy,這是Spring提供的一個(gè)Filter的代理,可以使用Spring bean容器中的一個(gè)bean來(lái)作為當(dāng)前的Filter實(shí)例,對(duì)應(yīng)的bean就會(huì)取filter-name對(duì)應(yīng)的那個(gè)bean。所以下面的配置會(huì)到bean容器中尋找一個(gè)名為shiroFilter的bean。
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
獨(dú)立使用Shiro時(shí)通常會(huì)定義一個(gè)org.apache.shiro.web.servlet.ShiroFilter來(lái)做類似的事。
接下來(lái)就是在bean容器中定義我們的shiroFilter了。如下我們定義了一個(gè)ShiroFilterFactoryBean,其會(huì)產(chǎn)生一個(gè)AbstractShiroFilter類型的bean。通過(guò)ShiroFilterFactoryBean我們可以指定一個(gè)SecurityManager,這里使用的DefaultWebSecurityManager需要指定一個(gè)Realm,如果需要指定多個(gè)Realm則通過(guò)realms指定。這里簡(jiǎn)單起見就直接使用基于文本定義的TextConfigurationRealm。通過(guò)loginUrl指定登錄地址、successUrl指定登錄成功后需要跳轉(zhuǎn)的地址,unauthorizedUrl指定權(quán)限不足時(shí)的提示頁(yè)面。filterChainDefinitions則定義URL與需要使用的Filter之間的關(guān)系,等號(hào)右邊的是filter的別名,默認(rèn)的別名都定義在org.apache.shiro.web.filter.mgt.DefaultFilter這個(gè)枚舉類中。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="successUrl" value="/home.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /admin/** = authc, roles[admin] /logout = logout # 其它地址都要求用戶已經(jīng)登錄了 /** = authc,logger </value> </property> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="realm"/> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 簡(jiǎn)單起見,這里就使用基于文本的Realm實(shí)現(xiàn) --> <bean id="realm" class="org.apache.shiro.realm.text.TextConfigurationRealm"> <property name="userDefinitions"> <value> user1=pass1,role1,role2 user2=pass2,role2,role3 admin=admin,admin </value> </property> </bean>
如果需要在filterChainDefinitions定義中使用自定義的Filter,則可以通過(guò)ShiroFilterFactoryBean的filters指定自定義的Filter及其別名映射關(guān)系。比如下面這樣我們新增了一個(gè)別名為logger的Filter,并在filterChainDefinitions中指定了/**需要應(yīng)用別名為logger的Filter。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="successUrl" value="/home.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filters"> <util:map> <entry key="logger"> <bean class="com.elim.chat.shiro.filter.LoggerFilter"/> </entry> </util:map> </property> <property name="filterChainDefinitions"> <value> /admin/** = authc, roles[admin] /logout = logout # 其它地址都要求用戶已經(jīng)登錄了 /** = authc,logger </value> </property> </bean>
其實(shí)我們需要應(yīng)用的Filter別名定義也可以不直接通過(guò)ShiroFilterFactoryBean的setFilters()來(lái)指定,而是直接在對(duì)應(yīng)的bean容器中定義對(duì)應(yīng)的Filter對(duì)應(yīng)的bean。因?yàn)槟J(rèn)情況下,ShiroFilterFactoryBean會(huì)把bean容器中的所有的Filter類型的bean以其id為別名注冊(cè)到filters中。所以上面的定義等價(jià)于下面這樣。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="successUrl" value="/home.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /admin/** = authc, roles[admin] /logout = logout # 其它地址都要求用戶已經(jīng)登錄了 /** = authc,logger </value> </property> </bean> <bean id="logger" class="com.elim.chat.shiro.filter.LoggerFilter"/>
經(jīng)過(guò)以上幾步,Shiro和Spring的整合就完成了,這個(gè)時(shí)候我們請(qǐng)求工程的任意路徑都會(huì)要求我們登錄,且會(huì)自動(dòng)跳轉(zhuǎn)到loginUrl指定的路徑讓我們輸入用戶名/密碼登錄。這個(gè)時(shí)候我們應(yīng)該提供一個(gè)表單,通過(guò)username獲得用戶名,通過(guò)password獲得密碼,然后提交登錄請(qǐng)求的時(shí)候請(qǐng)求需要提交到loginUrl指定的地址,但是請(qǐng)求方式需要變?yōu)镻OST。登錄時(shí)使用的用戶名/密碼是我們?cè)赥extConfigurationRealm中定義的用戶名/密碼,基于我們上面的配置則可以使用user1/pass1、admin/admin等。登錄成功后就會(huì)跳轉(zhuǎn)到successUrl參數(shù)指定的地址了。如果我們是使用user1/pass1登錄的,則我們還可以試著訪問(wèn)一下/admin/index,這個(gè)時(shí)候會(huì)因?yàn)闄?quán)限不足跳轉(zhuǎn)到unauthorized.jsp。
啟用基于注解的支持
基本的整合需要我們把URL需要應(yīng)用的權(quán)限控制都定義在ShiroFilterFactoryBean的filterChainDefinitions中。這有時(shí)候會(huì)沒(méi)那么靈活。Shiro為我們提供了整合Spring后可以使用的注解,它允許我們?cè)谛枰M(jìn)行權(quán)限控制的Class或Method上加上對(duì)應(yīng)的注解以定義訪問(wèn)Class或Method需要的權(quán)限,如果是定義中Class上的,則表示調(diào)用該Class中所有的方法都需要對(duì)應(yīng)的權(quán)限(注意需要是外部調(diào)用,這是動(dòng)態(tài)代理的局限)。要使用這些注解我們需要在Spring的bean容器中添加下面兩個(gè)bean定義,這樣才能在運(yùn)行時(shí)根據(jù)注解定義來(lái)判斷用戶是否擁有對(duì)應(yīng)的權(quán)限。這是通過(guò)Spring的AOP機(jī)制來(lái)實(shí)現(xiàn)的,關(guān)于Spring Aop如果有不是特別了解的,可以參考筆者寫在iteye的《Spring Aop介紹專欄》。下面的兩個(gè)bean定義,AuthorizationAttributeSourceAdvisor是定義了一個(gè)Advisor,其會(huì)基于Shiro提供的注解配置的方法進(jìn)行攔截,校驗(yàn)權(quán)限。DefaultAdvisorAutoProxyCreator則是提供了為標(biāo)注有Shiro提供的權(quán)限控制注解的Class創(chuàng)建代理對(duì)象,并在攔截到目標(biāo)方法調(diào)用時(shí)應(yīng)用AuthorizationAttributeSourceAdvisor的功能。當(dāng)攔截到了用戶的一個(gè)請(qǐng)求,而該用戶沒(méi)有對(duì)應(yīng)方法或類上標(biāo)注的權(quán)限時(shí),將拋出org.apache.shiro.authz.AuthorizationException異常。
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>
如果我們的bean容器中已經(jīng)定義了<aop:config/>或<aop:aspectj-autoproxy/>
,則可以不再定義DefaultAdvisorAutoProxyCreator。因?yàn)榍懊鎯煞N情況都會(huì)自動(dòng)添加與DefaultAdvisorAutoProxyCreator類似的bean。關(guān)于DefaultAdvisorAutoProxyCreator的更多介紹也可以參考筆者的Spring Aop自動(dòng)創(chuàng)建代理對(duì)象的原理這篇博客。
Shiro提供的權(quán)限控制注解如下:
RequiresAuthentication:需要用戶在當(dāng)前會(huì)話中是被認(rèn)證過(guò)的,即需要通過(guò)用戶名/密碼登錄過(guò),不包括RememberMe自動(dòng)登錄。
RequiresUser:需要用戶是被認(rèn)證過(guò)的,可以是在本次會(huì)話中通過(guò)用戶名/密碼登錄認(rèn)證,也可以是通過(guò)RememberMe自動(dòng)登錄。
RequiresGuest:需要用戶是未登錄的。
RequiresRoles:需要用戶擁有指定的角色。
RequiresPermissions:需要用戶擁有指定的權(quán)限。
前面三個(gè)都很好理解,而后面兩個(gè)是類似的。筆者這里拿@RequiresPermissions來(lái)做個(gè)示例。首先我們把上面定義的Realm改一下,給role添加權(quán)限。這樣我們的user1將擁有perm1、perm2和perm3的權(quán)限,而user2將擁有perm1、perm3和perm4的權(quán)限。
<bean id="realm" class="org.apache.shiro.realm.text.TextConfigurationRealm"> <property name="userDefinitions"> <value> user1=pass1,role1,role2 user2=pass2,role2,role3 admin=admin,admin </value> </property> <property name="roleDefinitions"> <value> role1=perm1,perm2 role2=perm1,perm3 role3=perm3,perm4 </value> </property> </bean>
@RequiresPermissions可以添加在方法上,用來(lái)指定調(diào)用該方法時(shí)需要擁有的權(quán)限。下面的代碼我們就指定了在訪問(wèn)/perm1時(shí)必須擁有perm1這個(gè)權(quán)限。這個(gè)時(shí)候user1和user2都能訪問(wèn)。
@RequestMapping("/perm1") @RequiresPermissions("perm1") public Object permission1() { return "permission1"; }
如果需要指定必須同時(shí)擁有多個(gè)權(quán)限才能訪問(wèn)某個(gè)方法,可以把需要指定的權(quán)限以數(shù)組的形式指定(注解上的數(shù)組屬性指定單個(gè)的時(shí)候可以不加大括號(hào),需要指定多個(gè)時(shí)就需要加大括號(hào))。比如下面這樣我們就指定了在訪問(wèn)/perm1AndPerm4時(shí)用戶必須同時(shí)擁有perm1和perm4這兩個(gè)權(quán)限。這時(shí)候就只有user2可以訪問(wèn),因?yàn)橹挥兴磐瑫r(shí)擁有perm1和perm4。
@RequestMapping("/perm1AndPerm4") @RequiresPermissions({"perm1", "perm4"}) public Object perm1AndPerm4() { return "perm1AndPerm4"; }
當(dāng)同時(shí)指定了多個(gè)權(quán)限時(shí),默認(rèn)多個(gè)權(quán)限之間的關(guān)系是與的關(guān)系,即需要同時(shí)擁有指定的所有的權(quán)限。如果只需要擁有指定的多個(gè)權(quán)限中的一個(gè)就可以訪問(wèn),則我們可以通過(guò)logical=Logical.OR指定多個(gè)權(quán)限之間是或的關(guān)系。比如下面這樣我們就指定了在訪問(wèn)/perm1OrPerm4時(shí)只需要擁有perm1或perm4權(quán)限即可,這樣user1和user2都可以訪問(wèn)該方法。
@RequestMapping("/perm1OrPerm4") @RequiresPermissions(value={"perm1", "perm4"}, logical=Logical.OR) public Object perm1OrPerm4() { return "perm1OrPerm4"; }
@RequiresPermissions也可以標(biāo)注在Class上,表示在外部訪問(wèn)Class中的方法時(shí)都需要有對(duì)應(yīng)的權(quán)限。比如下面這樣我們?cè)贑lass級(jí)別指定了需要擁有權(quán)限perm2,而在index()方法上則沒(méi)有指定需要任何權(quán)限,但是我們?cè)谠L問(wèn)該方法時(shí)還是需要擁有Class級(jí)別指定的權(quán)限。此時(shí)將只有user1可以訪問(wèn)。
@RestController @RequestMapping("/foo") @RequiresPermissions("perm2") public class FooController { @RequestMapping(method=RequestMethod.GET) public Object index() { Map<String, Object> map = new HashMap<>(); map.put("abc", 123); return map; } }
當(dāng)Class和方法級(jí)別都同時(shí)擁有@RequiresPermissions時(shí),方法級(jí)別的擁有更高的優(yōu)先級(jí),而且此時(shí)將只會(huì)校驗(yàn)方法級(jí)別要求的權(quán)限。如下我們?cè)贑lass級(jí)別指定了需要perm2權(quán)限,而在方法級(jí)別指定了需要perm3權(quán)限,那么在訪問(wèn)/foo時(shí)將只需要擁有perm3權(quán)限即可訪問(wèn)到index()方法。所以此時(shí)user1和user2都可以訪問(wèn)/foo。
@RestController @RequestMapping("/foo") @RequiresPermissions("perm2") public class FooController { @RequestMapping(method=RequestMethod.GET) @RequiresPermissions("perm3") public Object index() { Map<String, Object> map = new HashMap<>(); map.put("abc", 123); return map; } }
但是如果此時(shí)我們?cè)贑lass上新增@RequiresRoles("role1")指定需要擁有角色role1,那么此時(shí)訪問(wèn)/foo時(shí)需要擁有Class上的role1和index()方法上@RequiresPermissions("perm3")指定的perm3權(quán)限。因?yàn)镽equiresRoles和RequiresPermissions屬于不同維度的權(quán)限定義,Shiro在校驗(yàn)的時(shí)候都將校驗(yàn)一遍,但是如果Class和方法上都擁有同類型的權(quán)限控制定義的注解時(shí),則只會(huì)以方法上的定義為準(zhǔn)。
@RestController @RequestMapping("/foo") @RequiresPermissions("perm2") @RequiresRoles("role1") public class FooController { @RequestMapping(method=RequestMethod.GET) @RequiresPermissions("perm3") public Object index() { Map<String, Object> map = new HashMap<>(); map.put("abc", 123); return map; } }
雖然示例中使用的只是RequiresPermissions,但是其它權(quán)限控制注解的用法也是類似的,其它注解的用法請(qǐng)感興趣的朋友自己實(shí)踐。
基于注解控制權(quán)限的原理
上面使用@RequiresPermissions我們指定的權(quán)限都是靜態(tài)的,寫本文的一個(gè)主要目的是介紹一種方法,通過(guò)擴(kuò)展實(shí)現(xiàn)來(lái)使指定的權(quán)限可以是動(dòng)態(tài)的。但是在擴(kuò)展前我們得知道它底層的工作方式,即實(shí)現(xiàn)原理,我們才能進(jìn)行擴(kuò)展。所以接下來(lái)我們先來(lái)看一下Shiro整合Spring后使用@RequiresPermissions的工作原理。在啟用對(duì)@RequiresPermissions的支持時(shí)我們定義了如下bean,這是一個(gè)Advisor,其繼承自StaticMethodMatcherPointcutAdvisor,它的方法匹配邏輯是只要Class或Method上擁有Shiro的幾個(gè)權(quán)限控制注解即可,而攔截以后的處理邏輯則是由相應(yīng)的Advice指定。
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>
以下是AuthorizationAttributeSourceAdvisor的源碼。我們可以看到在其構(gòu)造方法中通過(guò)setAdvice()指定了AopAllianceAnnotationsAuthorizingMethodInterceptor這個(gè)Advice實(shí)現(xiàn)類,這是基于MethodInterceptor的實(shí)現(xiàn)。
public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor { private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class); private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES = new Class[] { RequiresPermissions.class, RequiresRoles.class, RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class }; protected SecurityManager securityManager = null; public AuthorizationAttributeSourceAdvisor() { setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor()); } public SecurityManager getSecurityManager() { return securityManager; } public void setSecurityManager(org.apache.shiro.mgt.SecurityManager securityManager) { this.securityManager = securityManager; } public boolean matches(Method method, Class targetClass) { Method m = method; if ( isAuthzAnnotationPresent(m) ) { return true; } //The 'method' parameter could be from an interface that doesn't have the annotation. //Check to see if the implementation has it. if ( targetClass != null) { try { m = targetClass.getMethod(m.getName(), m.getParameterTypes()); return isAuthzAnnotationPresent(m) || isAuthzAnnotationPresent(targetClass); } catch (NoSuchMethodException ignored) { //default return value is false. If we can't find the method, then obviously //there is no annotation, so just use the default return value. } } return false; } private boolean isAuthzAnnotationPresent(Class<?> targetClazz) { for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) { Annotation a = AnnotationUtils.findAnnotation(targetClazz, annClass); if ( a != null ) { return true; } } return false; } private boolean isAuthzAnnotationPresent(Method method) { for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) { Annotation a = AnnotationUtils.findAnnotation(method, annClass); if ( a != null ) { return true; } } return false; } }
AopAllianceAnnotationsAuthorizingMethodInterceptor的源碼如下。其實(shí)現(xiàn)的MethodInterceptor接口的invoke方法又調(diào)用了父類的invoke方法。同時(shí)我們要看到在其構(gòu)造方法中創(chuàng)建了一些AuthorizingAnnotationMethodInterceptor實(shí)現(xiàn),這些實(shí)現(xiàn)才是實(shí)現(xiàn)權(quán)限控制的核心,待會(huì)我們會(huì)挑出PermissionAnnotationMethodInterceptor實(shí)現(xiàn)類來(lái)看其具體的實(shí)現(xiàn)邏輯。
public class AopAllianceAnnotationsAuthorizingMethodInterceptor extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor { public AopAllianceAnnotationsAuthorizingMethodInterceptor() { List<AuthorizingAnnotationMethodInterceptor> interceptors = new ArrayList<AuthorizingAnnotationMethodInterceptor>(5); //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the //raw JDK resolution process. AnnotationResolver resolver = new SpringAnnotationResolver(); //we can re-use the same resolver instance - it does not retain state: interceptors.add(new RoleAnnotationMethodInterceptor(resolver)); interceptors.add(new PermissionAnnotationMethodInterceptor(resolver)); interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver)); interceptors.add(new UserAnnotationMethodInterceptor(resolver)); interceptors.add(new GuestAnnotationMethodInterceptor(resolver)); setMethodInterceptors(interceptors); } protected org.apache.shiro.aop.MethodInvocation createMethodInvocation(Object implSpecificMethodInvocation) { final MethodInvocation mi = (MethodInvocation) implSpecificMethodInvocation; return new org.apache.shiro.aop.MethodInvocation() { public Method getMethod() { return mi.getMethod(); } public Object[] getArguments() { return mi.getArguments(); } public String toString() { return "Method invocation [" + mi.getMethod() + "]"; } public Object proceed() throws Throwable { return mi.proceed(); } public Object getThis() { return mi.getThis(); } }; } protected Object continueInvocation(Object aopAllianceMethodInvocation) throws Throwable { MethodInvocation mi = (MethodInvocation) aopAllianceMethodInvocation; return mi.proceed(); } public Object invoke(MethodInvocation methodInvocation) throws Throwable { org.apache.shiro.aop.MethodInvocation mi = createMethodInvocation(methodInvocation); return super.invoke(mi); } }
通過(guò)看父類的invoke方法實(shí)現(xiàn),最終我們會(huì)看到核心邏輯是調(diào)用assertAuthorized方法,而該方法的實(shí)現(xiàn)(源碼如下)又是依次判斷配置的AuthorizingAnnotationMethodInterceptor是否支持當(dāng)前方法進(jìn)行權(quán)限校驗(yàn)(通過(guò)判斷Class或Method上是否擁有其支持的注解),當(dāng)支持時(shí)則會(huì)調(diào)用其assertAuthorized方法進(jìn)行權(quán)限校驗(yàn),而AuthorizingAnnotationMethodInterceptor又會(huì)調(diào)用AuthorizingAnnotationHandler的assertAuthorized方法。
protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException { //default implementation just ensures no deny votes are cast: Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors(); if (aamis != null && !aamis.isEmpty()) { for (AuthorizingAnnotationMethodInterceptor aami : aamis) { if (aami.supports(methodInvocation)) { aami.assertAuthorized(methodInvocation); } } } }
接下來(lái)我們?cè)倩剡^(guò)頭來(lái)看AopAllianceAnnotationsAuthorizingMethodInterceptor的定義的PermissionAnnotationMethodInterceptor,其源碼如下。結(jié)合AopAllianceAnnotationsAuthorizingMethodInterceptor的源碼和PermissionAnnotationMethodInterceptor的源碼,我們可以看到PermissionAnnotationMethodInterceptor中這時(shí)候指定了PermissionAnnotationHandler和SpringAnnotationResolver。PermissionAnnotationHandler是AuthorizingAnnotationHandler的一個(gè)子類。所以我們最終的權(quán)限控制由PermissionAnnotationHandler的assertAuthorized實(shí)現(xiàn)決定。
public class PermissionAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor { public PermissionAnnotationMethodInterceptor() { super( new PermissionAnnotationHandler() ); } public PermissionAnnotationMethodInterceptor(AnnotationResolver resolver) { super( new PermissionAnnotationHandler(), resolver); } }
接下來(lái)我們來(lái)看PermissionAnnotationHandler的assertAuthorized方法實(shí)現(xiàn),其完整代碼如下。從實(shí)現(xiàn)上我們可以看到其會(huì)從Annotation中獲取配置的權(quán)限值,而這里的Annotation就是RequiresPermissions注解。而且在進(jìn)行權(quán)限校驗(yàn)時(shí)都是直接使用的我們定義注解時(shí)指定的文本值,待會(huì)我們進(jìn)行擴(kuò)展時(shí)就將從這里入手。
public class PermissionAnnotationHandler extends AuthorizingAnnotationHandler { public PermissionAnnotationHandler() { super(RequiresPermissions.class); } protected String[] getAnnotationValue(Annotation a) { RequiresPermissions rpAnnotation = (RequiresPermissions) a; return rpAnnotation.value(); } public void assertAuthorized(Annotation a) throws AuthorizationException { if (!(a instanceof RequiresPermissions)) return; RequiresPermissions rpAnnotation = (RequiresPermissions) a; String[] perms = getAnnotationValue(a); Subject subject = getSubject(); if (perms.length == 1) { subject.checkPermission(perms[0]); return; } if (Logical.AND.equals(rpAnnotation.logical())) { getSubject().checkPermissions(perms); return; } if (Logical.OR.equals(rpAnnotation.logical())) { // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first boolean hasAtLeastOnePermission = false; for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true; // Cause the exception if none of the role match, note that the exception message will be a bit misleading if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]); } } }
通過(guò)前面的介紹我們知道PermissionAnnotationHandler的assertAuthorized方法參數(shù)的Annotation是由AuthorizingAnnotationMethodInterceptor在調(diào)用AuthorizingAnnotationHandler的assertAuthorized方法時(shí)傳遞的。其源碼如下,從源碼中我們可以看到Annotation是通過(guò)getAnnotation方法獲得的。
public void assertAuthorized(MethodInvocation mi) throws AuthorizationException { try { ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi)); } catch(AuthorizationException ae) { if (ae.getCause() == null) ae.initCause(new AuthorizationException("Not authorized to invoke method: " + mi.getMethod())); throw ae; } }
沿著這個(gè)方向走下去,最終我們會(huì)找到SpringAnnotationResolver的getAnnotation方法實(shí)現(xiàn),其實(shí)現(xiàn)如下。從下面的代碼可以看到,其在尋找注解時(shí)是優(yōu)先尋找Method上的,如果在Method上沒(méi)有找到會(huì)從當(dāng)前方法調(diào)用的所屬Class上尋找對(duì)應(yīng)的注解。從這里也可以看到為什么我們之前在Class和Method上都定義了相同類型的權(quán)限控制注解時(shí)生效的是Method上的,而單獨(dú)存在的時(shí)候就是單獨(dú)定義的那個(gè)生效了。
public class SpringAnnotationResolver implements AnnotationResolver { public Annotation getAnnotation(MethodInvocation mi, Class<? extends Annotation> clazz) { Method m = mi.getMethod(); Annotation a = AnnotationUtils.findAnnotation(m, clazz); if (a != null) return a; //The MethodInvocation's method object could be a method defined in an interface. //However, if the annotation existed in the interface's implementation (and not //the interface itself), it won't be on the above method object. Instead, we need to //acquire the method representation from the targetClass and check directly on the //implementation itself: Class<?> targetClass = mi.getThis().getClass(); m = ClassUtils.getMostSpecificMethod(m, targetClass); a = AnnotationUtils.findAnnotation(m, clazz); if (a != null) return a; // See if the class has the same annotation return AnnotationUtils.findAnnotation(mi.getThis().getClass(), clazz); } }
通過(guò)以上的源碼閱讀,相信讀者對(duì)于Shiro整合Spring后支持的權(quán)限控制注解的原理已經(jīng)有了比較深入的理解。上面貼出的源碼只是部分筆者認(rèn)為比較核心的,有想詳細(xì)了解完整內(nèi)容的請(qǐng)讀者自己沿著筆者提到的思路去閱讀完整代碼。
了解了這塊基于注解進(jìn)行權(quán)限控制的原理后,讀者朋友們也可以根據(jù)實(shí)際的業(yè)務(wù)需要進(jìn)行相應(yīng)的擴(kuò)展。
擴(kuò)展使用Spring EL表達(dá)式
假設(shè)現(xiàn)在內(nèi)部有下面這樣一個(gè)接口,其中有一個(gè)query方法,接收一個(gè)參數(shù)type。這里我們簡(jiǎn)化一點(diǎn),假設(shè)只要接收這么一個(gè)參數(shù),然后對(duì)應(yīng)不同的取值時(shí)將返回不同的結(jié)果。
public interface RealService { Object query(int type); }
這個(gè)接口是對(duì)外開放的,通過(guò)對(duì)應(yīng)的URL可以請(qǐng)求到該方法,我們定義了對(duì)應(yīng)的Controller方法如下:
@RequestMapping("/service/{type}") public Object query(@PathVariable("type") int type) { return this.realService.query(type); }
上面的接口服務(wù)在進(jìn)行查詢的時(shí)候針對(duì)type是有權(quán)限的,不是每個(gè)用戶都可以使用每種type進(jìn)行查詢的,需要擁有對(duì)應(yīng)的權(quán)限才行。所以針對(duì)上面的處理器方法我們需要加上權(quán)限控制,而且在控制時(shí)需要的權(quán)限是隨著參數(shù)type動(dòng)態(tài)變的。假設(shè)關(guān)于type的每項(xiàng)權(quán)限的定義是query:type的形式,比如type=1時(shí)需要的權(quán)限是query:1,type=2時(shí)需要的權(quán)限是query:2。在沒(méi)有與Spring整合時(shí),我們會(huì)如下這樣做:
@RequestMapping("/service/{type}") public Object query(@PathVariable("type") int type) { SecurityUtils.getSubject().checkPermission("query:" + type); return this.realService.query(type); }
但是與Spring整合后,上面的做法耦合性強(qiáng),我們會(huì)更希望通過(guò)整合后的注解來(lái)進(jìn)行權(quán)限控制。對(duì)于上面的場(chǎng)景我們更希望通過(guò)@RequiresPermissions來(lái)指定需要的權(quán)限,但是@RequiresPermissions中定義的權(quán)限是靜態(tài)文本,固定的。它沒(méi)法滿足我們動(dòng)態(tài)的需求。這個(gè)時(shí)候可能你會(huì)想著我們可以把Controller處理方法拆分為多個(gè),單獨(dú)進(jìn)行權(quán)限控制。比如下面這樣:
@RequestMapping("/service/1") @RequiresPermissions("query:1") public Object service1() { return this.realService.query(1); } @RequiresPermissions("query:2") @RequestMapping("/service/2") public Object service2() { return this.realService.query(2); } //... @RequestMapping("/service/200") @RequiresPermissions("query:200") public Object service200() { return this.realService.query(200); }
這在type的取值范圍比較小的時(shí)候還可以,但是如果像上面這樣可能的取值有200種,把它們窮舉出來(lái)定義單獨(dú)的處理器方法并進(jìn)行權(quán)限控制就顯得有點(diǎn)麻煩了。另外就是如果將來(lái)type的取值有變動(dòng),我們還得添加新的處理器方法。所以最好的辦法是讓@RequiresPermissions支持動(dòng)態(tài)的權(quán)限定義,同時(shí)又可以維持靜態(tài)定義的支持。通過(guò)前面的分析我們知道,切入點(diǎn)是PermissionAnnotationHandler,而它里面是沒(méi)有提供對(duì)權(quán)限校驗(yàn)的擴(kuò)展的。我們?nèi)绻雽?duì)它擴(kuò)展簡(jiǎn)單的辦法就是把它整體的替換。但是我們需要?jiǎng)討B(tài)處理的權(quán)限是跟方法參數(shù)相關(guān)的,而PermissionAnnotationHandler中是取不到方法參數(shù)的,為此我們不能直接替換掉PermissionAnnotationHandler。PermissionAnnotationHandler是由PermissionAnnotationMethodInterceptor調(diào)用的,在其父類AuthorizingAnnotationMethodInterceptor的assertAuthorized方法中調(diào)用PermissionAnnotationHandler時(shí)是可以獲取到方法參數(shù)的。為此我們的擴(kuò)展點(diǎn)就選在PermissionAnnotationMethodInterceptor類上,我們也需要把它整體的替換。Spring的EL表達(dá)式可以支持解析方法參數(shù)值,這里我們選擇引入Spring的EL表達(dá)式,在@RequiresPermissions定義權(quán)限時(shí)可以使用Spring EL表達(dá)式引入方法參數(shù)。同時(shí)為了兼顧靜態(tài)的文本。這里引入Spring的EL表達(dá)式模板。關(guān)于Spring的EL表達(dá)式模板可以參考筆者的這篇博文。我們定義自己的PermissionAnnotationMethodInterceptor,把它繼承自PermissionAnnotationMethodInterceptor,重寫assertAuthoried方法,方法的實(shí)現(xiàn)邏輯參考PermissionAnnotationHandler中的邏輯,但是所使用的@RequiresPermissions中的權(quán)限定義,是我們使用Spring EL表達(dá)式基于當(dāng)前調(diào)用的方法作為EvaluationContext解析后的結(jié)果。以下是我們自己定義的PermissionAnnotationMethodInterceptor實(shí)現(xiàn)。
public class SelfPermissionAnnotationMethodInterceptor extends PermissionAnnotationMethodInterceptor { private final SpelExpressionParser parser = new SpelExpressionParser(); private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer(); private final TemplateParserContext templateParserContext = new TemplateParserContext(); public SelfPermissionAnnotationMethodInterceptor(AnnotationResolver resolver) { super(resolver); } @Override public void assertAuthorized(MethodInvocation mi) throws AuthorizationException { Annotation annotation = super.getAnnotation(mi); RequiresPermissions permAnnotation = (RequiresPermissions) annotation; String[] perms = permAnnotation.value(); EvaluationContext evaluationContext = new MethodBasedEvaluationContext(null, mi.getMethod(), mi.getArguments(), paramNameDiscoverer); for (int i=0; i<perms.length; i++) { Expression expression = this.parser.parseExpression(perms[i], templateParserContext); //使用Spring EL表達(dá)式解析后的權(quán)限定義替換原來(lái)的權(quán)限定義 perms[i] = expression.getValue(evaluationContext, String.class); } Subject subject = getSubject(); if (perms.length == 1) { subject.checkPermission(perms[0]); return; } if (Logical.AND.equals(permAnnotation.logical())) { getSubject().checkPermissions(perms); return; } if (Logical.OR.equals(permAnnotation.logical())) { // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first boolean hasAtLeastOnePermission = false; for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true; // Cause the exception if none of the role match, note that the exception message will be a bit misleading if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]); } } }
定義了自己的PermissionAnnotationMethodInterceptor后,我們需要替換原來(lái)的PermissionAnnotationMethodInterceptor為我們自己的PermissionAnnotationMethodInterceptor。根據(jù)前面介紹的Shiro整合Spring后使用@RequiresPermissions等注解的原理我們知道PermissionAnnotationMethodInterceptor是由AopAllianceAnnotationsAuthorizingMethodInterceptor指定的,而后者又是由AuthorizationAttributeSourceAdvisor指定的。為此我們需要在定義AuthorizationAttributeSourceAdvisor時(shí)通過(guò)顯示定義AopAllianceAnnotationsAuthorizingMethodInterceptor的方式顯示的定義其中的AuthorizingAnnotationMethodInterceptor,然后把自帶的PermissionAnnotationMethodInterceptor替換為我們自定義的SelfAuthorizingAnnotationMethodInterceptor。替換后的定義如下:
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> <property name="advice"> <bean class="org.apache.shiro.spring.security.interceptor.AopAllianceAnnotationsAuthorizingMethodInterceptor"> <property name="methodInterceptors"> <util:list> <bean class="org.apache.shiro.authz.aop.RoleAnnotationMethodInterceptor" c:resolver-ref="springAnnotationResolver"/> <!-- 使用自定義的PermissionAnnotationMethodInterceptor --> <bean class="com.elim.chat.shiro.SelfPermissionAnnotationMethodInterceptor" c:resolver-ref="springAnnotationResolver"/> <bean class="org.apache.shiro.authz.aop.AuthenticatedAnnotationMethodInterceptor" c:resolver-ref="springAnnotationResolver"/> <bean class="org.apache.shiro.authz.aop.UserAnnotationMethodInterceptor" c:resolver-ref="springAnnotationResolver"/> <bean class="org.apache.shiro.authz.aop.GuestAnnotationMethodInterceptor" c:resolver-ref="springAnnotationResolver"/> </util:list> </property> </bean> </property> </bean> <bean id="springAnnotationResolver" class="org.apache.shiro.spring.aop.SpringAnnotationResolver"/>
為了演示前面示例的動(dòng)態(tài)的權(quán)限,我們把角色與權(quán)限的關(guān)系調(diào)整如下,讓role1、role2和role3分別擁有query:1、query:2和query:3的權(quán)限。此時(shí)user1將擁有query:1和query:2的權(quán)限。
<bean id="realm" class="org.apache.shiro.realm.text.TextConfigurationRealm"> <property name="userDefinitions"> <value> user1=pass1,role1,role2 user2=pass2,role2,role3 admin=admin,admin </value> </property> <property name="roleDefinitions"> <value> role1=perm1,perm2,query:1 role2=perm1,perm3,query:2 role3=perm3,perm4,query:3 </value> </property> </bean>
此時(shí)@RequiresPermissions中指定權(quán)限時(shí)就可以使用Spring EL表達(dá)式支持的語(yǔ)法了。因?yàn)槲覀冊(cè)诙xSelfPermissionAnnotationMethodInterceptor時(shí)已經(jīng)指定了應(yīng)用基于模板的表達(dá)式解析,此時(shí)權(quán)限中定義的文本都將作為文本解析,動(dòng)態(tài)的部分默認(rèn)需要使用#{前綴和}后綴包起來(lái)(這個(gè)前綴和后綴是可以指定的,但是默認(rèn)就好)。在動(dòng)態(tài)部分中可以使用#前綴引用變量,基于方法的表達(dá)式解析中可以使用參數(shù)名或p參數(shù)索引的形式引用方法參數(shù)。所以上面我們需要?jiǎng)討B(tài)的權(quán)限的query方法的@RequiresPermissions定義如下。
@RequestMapping("/service/{type}") @RequiresPermissions("query:#{#type}") public Object query(@PathVariable("type") int type) { return this.realService.query(type); }
這樣user1在訪問(wèn)/service/1和/service/2是OK的,但是在訪問(wèn)/service/3和/service/300時(shí)會(huì)提示沒(méi)有權(quán)限,因?yàn)閡ser1沒(méi)有query:3和query:300的權(quán)限。
總結(jié)
以上所述是小編給大家介紹的Spring 整合Shiro 并擴(kuò)展使用EL表達(dá)式的實(shí)例詳解,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(28)
下面小編就為大家?guī)?lái)一篇Java基礎(chǔ)的幾道練習(xí)題(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,希望可以幫到你2021-07-07SpringCloud Gateway使用redis實(shí)現(xiàn)動(dòng)態(tài)路由的方法
這篇文章主要介紹了SpringCloud Gateway使用redis實(shí)現(xiàn)動(dòng)態(tài)路由的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01springboot+zookeeper實(shí)現(xiàn)分布式鎖的示例代碼
本文主要介紹了springboot+zookeeper實(shí)現(xiàn)分布式鎖的示例代碼,文中根據(jù)實(shí)例編碼詳細(xì)介紹的十分詳盡,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03淺談java中的一維數(shù)組、二維數(shù)組、三維數(shù)組、多維數(shù)組
下面小編就為大家?guī)?lái)一篇淺談java中的一維數(shù)組、二維數(shù)組、三維數(shù)組、多維數(shù)組。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05java單元測(cè)試JUnit框架原理與用法實(shí)例教程
這篇文章主要介紹了java單元測(cè)試JUnit框架原理與用法,結(jié)合實(shí)例形式較為詳細(xì)的分析了java單元測(cè)試JUnit框架的概念、原理、使用方法及相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-11-11Spring?Boot快速過(guò)濾出一次請(qǐng)求的所有日志
這篇文章主要介紹了Spring?Boot快速過(guò)濾出一次請(qǐng)求的所有日志,本文講述了如何使用MDC工具來(lái)快速過(guò)濾一次請(qǐng)求的所有日志,并通過(guò)裝飾器模式使得MDC工具在異步線程里也能生效,需要的朋友可以參考下2022-11-11