您好,登錄后才能下訂單哦!
小編給大家分享一下Spring整合Shiro并擴展使用EL表達式的示例分析,希望大家閱讀完這篇文章之后都有所收獲,下面讓我們一起去探討吧!
Shiro是一個輕量級的權限控制框架,應用非常廣泛。本文的重點是介紹Spring整合Shiro,并通過擴展使用Spring的EL表達式,使@RequiresRoles等支持動態的參數。對Shiro的介紹則不在本文的討論范圍之內,讀者如果有對shiro不是很了解的,可以通過其官方網站了解相應的信息。
Shiro整合Spring
首先需要在你的工程中加入shiro-spring-xxx.jar,如果是使用Maven管理你的工程,則可以在你的依賴中加入以下依賴,筆者這里是選擇的當前最新的1.4.0版本。
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
接下來需要在你的web.xml中定義一個shiroFilter,應用它來攔截所有的需要權限控制的請求,通常是配置為/*。另外該Filter需要加入最前面,以確保請求進來后最先通過shiro的權限控制。這里的Filter對應的class配置的是DelegatingFilterProxy,這是Spring提供的一個Filter的代理,可以使用Spring bean容器中的一個bean來作為當前的Filter實例,對應的bean就會取filter-name對應的那個bean。所以下面的配置會到bean容器中尋找一個名為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>
獨立使用Shiro時通常會定義一個org.apache.shiro.web.servlet.ShiroFilter來做類似的事。
接下來就是在bean容器中定義我們的shiroFilter了。如下我們定義了一個ShiroFilterFactoryBean,其會產生一個AbstractShiroFilter類型的bean。通過ShiroFilterFactoryBean我們可以指定一個SecurityManager,這里使用的DefaultWebSecurityManager需要指定一個Realm,如果需要指定多個Realm則通過realms指定。這里簡單起見就直接使用基于文本定義的TextConfigurationRealm。通過loginUrl指定登錄地址、successUrl指定登錄成功后需要跳轉的地址,unauthorizedUrl指定權限不足時的提示頁面。filterChainDefinitions則定義URL與需要使用的Filter之間的關系,等號右邊的是filter的別名,默認的別名都定義在org.apache.shiro.web.filter.mgt.DefaultFilter這個枚舉類中。
<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 # 其它地址都要求用戶已經登錄了 /** = 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"/>
<!-- 簡單起見,這里就使用基于文本的Realm實現 --> <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,則可以通過ShiroFilterFactoryBean的filters指定自定義的Filter及其別名映射關系。比如下面這樣我們新增了一個別名為logger的Filter,并在filterChainDefinitions中指定了/**需要應用別名為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 # 其它地址都要求用戶已經登錄了 /** = authc,logger </value> </property> </bean>
其實我們需要應用的Filter別名定義也可以不直接通過ShiroFilterFactoryBean的setFilters()來指定,而是直接在對應的bean容器中定義對應的Filter對應的bean。因為默認情況下,ShiroFilterFactoryBean會把bean容器中的所有的Filter類型的bean以其id為別名注冊到filters中。所以上面的定義等價于下面這樣。
<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 # 其它地址都要求用戶已經登錄了 /** = authc,logger </value> </property> </bean> <bean id="logger" class="com.elim.chat.shiro.filter.LoggerFilter"/>
經過以上幾步,Shiro和Spring的整合就完成了,這個時候我們請求工程的任意路徑都會要求我們登錄,且會自動跳轉到loginUrl指定的路徑讓我們輸入用戶名/密碼登錄。這個時候我們應該提供一個表單,通過username獲得用戶名,通過password獲得密碼,然后提交登錄請求的時候請求需要提交到loginUrl指定的地址,但是請求方式需要變為POST。登錄時使用的用戶名/密碼是我們在TextConfigurationRealm中定義的用戶名/密碼,基于我們上面的配置則可以使用user1/pass1、admin/admin等。登錄成功后就會跳轉到successUrl參數指定的地址了。如果我們是使用user1/pass1登錄的,則我們還可以試著訪問一下/admin/index,這個時候會因為權限不足跳轉到unauthorized.jsp。
啟用基于注解的支持
基本的整合需要我們把URL需要應用的權限控制都定義在ShiroFilterFactoryBean的filterChainDefinitions中。這有時候會沒那么靈活。Shiro為我們提供了整合Spring后可以使用的注解,它允許我們在需要進行權限控制的Class或Method上加上對應的注解以定義訪問Class或Method需要的權限,如果是定義中Class上的,則表示調用該Class中所有的方法都需要對應的權限(注意需要是外部調用,這是動態代理的局限)。要使用這些注解我們需要在Spring的bean容器中添加下面兩個bean定義,這樣才能在運行時根據注解定義來判斷用戶是否擁有對應的權限。這是通過Spring的AOP機制來實現的,關于Spring Aop如果有不是特別了解的,可以參考筆者寫在iteye的《Spring Aop介紹專欄》。下面的兩個bean定義,AuthorizationAttributeSourceAdvisor是定義了一個Advisor,其會基于Shiro提供的注解配置的方法進行攔截,校驗權限。DefaultAdvisorAutoProxyCreator則是提供了為標注有Shiro提供的權限控制注解的Class創建代理對象,并在攔截到目標方法調用時應用AuthorizationAttributeSourceAdvisor的功能。當攔截到了用戶的一個請求,而該用戶沒有對應方法或類上標注的權限時,將拋出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容器中已經定義了<aop:config/>或<aop:aspectj-autoproxy/>
,則可以不再定義DefaultAdvisorAutoProxyCreator。因為前面兩種情況都會自動添加與DefaultAdvisorAutoProxyCreator類似的bean。關于DefaultAdvisorAutoProxyCreator的更多介紹也可以參考筆者的Spring Aop自動創建代理對象的原理這篇博客。
Shiro提供的權限控制注解如下:
RequiresAuthentication:需要用戶在當前會話中是被認證過的,即需要通過用戶名/密碼登錄過,不包括RememberMe自動登錄。
RequiresUser:需要用戶是被認證過的,可以是在本次會話中通過用戶名/密碼登錄認證,也可以是通過RememberMe自動登錄。
RequiresGuest:需要用戶是未登錄的。
RequiresRoles:需要用戶擁有指定的角色。
RequiresPermissions:需要用戶擁有指定的權限。
前面三個都很好理解,而后面兩個是類似的。筆者這里拿@RequiresPermissions來做個示例。首先我們把上面定義的Realm改一下,給role添加權限。這樣我們的user1將擁有perm1、perm2和perm3的權限,而user2將擁有perm1、perm3和perm4的權限。
<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可以添加在方法上,用來指定調用該方法時需要擁有的權限。下面的代碼我們就指定了在訪問/perm1時必須擁有perm1這個權限。這個時候user1和user2都能訪問。
@RequestMapping("/perm1") @RequiresPermissions("perm1") public Object permission1() { return "permission1"; }
如果需要指定必須同時擁有多個權限才能訪問某個方法,可以把需要指定的權限以數組的形式指定(注解上的數組屬性指定單個的時候可以不加大括號,需要指定多個時就需要加大括號)。比如下面這樣我們就指定了在訪問/perm1AndPerm4時用戶必須同時擁有perm1和perm4這兩個權限。這時候就只有user2可以訪問,因為只有它才同時擁有perm1和perm4。
@RequestMapping("/perm1AndPerm4") @RequiresPermissions({"perm1", "perm4"}) public Object perm1AndPerm4() { return "perm1AndPerm4"; }
當同時指定了多個權限時,默認多個權限之間的關系是與的關系,即需要同時擁有指定的所有的權限。如果只需要擁有指定的多個權限中的一個就可以訪問,則我們可以通過logical=Logical.OR指定多個權限之間是或的關系。比如下面這樣我們就指定了在訪問/perm1OrPerm4時只需要擁有perm1或perm4權限即可,這樣user1和user2都可以訪問該方法。
@RequestMapping("/perm1OrPerm4") @RequiresPermissions(value={"perm1", "perm4"}, logical=Logical.OR) public Object perm1OrPerm4() { return "perm1OrPerm4"; }
@RequiresPermissions也可以標注在Class上,表示在外部訪問Class中的方法時都需要有對應的權限。比如下面這樣我們在Class級別指定了需要擁有權限perm2,而在index()方法上則沒有指定需要任何權限,但是我們在訪問該方法時還是需要擁有Class級別指定的權限。此時將只有user1可以訪問。
@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; } }
當Class和方法級別都同時擁有@RequiresPermissions時,方法級別的擁有更高的優先級,而且此時將只會校驗方法級別要求的權限。如下我們在Class級別指定了需要perm2權限,而在方法級別指定了需要perm3權限,那么在訪問/foo時將只需要擁有perm3權限即可訪問到index()方法。所以此時user1和user2都可以訪問/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; } }
但是如果此時我們在Class上新增@RequiresRoles("role1")指定需要擁有角色role1,那么此時訪問/foo時需要擁有Class上的role1和index()方法上@RequiresPermissions("perm3")指定的perm3權限。因為RequiresRoles和RequiresPermissions屬于不同維度的權限定義,Shiro在校驗的時候都將校驗一遍,但是如果Class和方法上都擁有同類型的權限控制定義的注解時,則只會以方法上的定義為準。
@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,但是其它權限控制注解的用法也是類似的,其它注解的用法請感興趣的朋友自己實踐。
基于注解控制權限的原理
上面使用@RequiresPermissions我們指定的權限都是靜態的,寫本文的一個主要目的是介紹一種方法,通過擴展實現來使指定的權限可以是動態的。但是在擴展前我們得知道它底層的工作方式,即實現原理,我們才能進行擴展。所以接下來我們先來看一下Shiro整合Spring后使用@RequiresPermissions的工作原理。在啟用對@RequiresPermissions的支持時我們定義了如下bean,這是一個Advisor,其繼承自StaticMethodMatcherPointcutAdvisor,它的方法匹配邏輯是只要Class或Method上擁有Shiro的幾個權限控制注解即可,而攔截以后的處理邏輯則是由相應的Advice指定。
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>
以下是AuthorizationAttributeSourceAdvisor的源碼。我們可以看到在其構造方法中通過setAdvice()指定了AopAllianceAnnotationsAuthorizingMethodInterceptor這個Advice實現類,這是基于MethodInterceptor的實現。
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的源碼如下。其實現的MethodInterceptor接口的invoke方法又調用了父類的invoke方法。同時我們要看到在其構造方法中創建了一些AuthorizingAnnotationMethodInterceptor實現,這些實現才是實現權限控制的核心,待會我們會挑出PermissionAnnotationMethodInterceptor實現類來看其具體的實現邏輯。
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); } }
通過看父類的invoke方法實現,最終我們會看到核心邏輯是調用assertAuthorized方法,而該方法的實現(源碼如下)又是依次判斷配置的AuthorizingAnnotationMethodInterceptor是否支持當前方法進行權限校驗(通過判斷Class或Method上是否擁有其支持的注解),當支持時則會調用其assertAuthorized方法進行權限校驗,而AuthorizingAnnotationMethodInterceptor又會調用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); } } } }
接下來我們再回過頭來看AopAllianceAnnotationsAuthorizingMethodInterceptor的定義的PermissionAnnotationMethodInterceptor,其源碼如下。結合AopAllianceAnnotationsAuthorizingMethodInterceptor的源碼和PermissionAnnotationMethodInterceptor的源碼,我們可以看到PermissionAnnotationMethodInterceptor中這時候指定了PermissionAnnotationHandler和SpringAnnotationResolver。PermissionAnnotationHandler是AuthorizingAnnotationHandler的一個子類。所以我們最終的權限控制由PermissionAnnotationHandler的assertAuthorized實現決定。
public class PermissionAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor { public PermissionAnnotationMethodInterceptor() { super( new PermissionAnnotationHandler() ); } public PermissionAnnotationMethodInterceptor(AnnotationResolver resolver) { super( new PermissionAnnotationHandler(), resolver); } }
接下來我們來看PermissionAnnotationHandler的assertAuthorized方法實現,其完整代碼如下。從實現上我們可以看到其會從Annotation中獲取配置的權限值,而這里的Annotation就是RequiresPermissions注解。而且在進行權限校驗時都是直接使用的我們定義注解時指定的文本值,待會我們進行擴展時就將從這里入手。
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]); } } }
通過前面的介紹我們知道PermissionAnnotationHandler的assertAuthorized方法參數的Annotation是由AuthorizingAnnotationMethodInterceptor在調用AuthorizingAnnotationHandler的assertAuthorized方法時傳遞的。其源碼如下,從源碼中我們可以看到Annotation是通過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; } }
沿著這個方向走下去,最終我們會找到SpringAnnotationResolver的getAnnotation方法實現,其實現如下。從下面的代碼可以看到,其在尋找注解時是優先尋找Method上的,如果在Method上沒有找到會從當前方法調用的所屬Class上尋找對應的注解。從這里也可以看到為什么我們之前在Class和Method上都定義了相同類型的權限控制注解時生效的是Method上的,而單獨存在的時候就是單獨定義的那個生效了。
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); } }
通過以上的源碼閱讀,相信讀者對于Shiro整合Spring后支持的權限控制注解的原理已經有了比較深入的理解。上面貼出的源碼只是部分筆者認為比較核心的,有想詳細了解完整內容的請讀者自己沿著筆者提到的思路去閱讀完整代碼。
了解了這塊基于注解進行權限控制的原理后,讀者朋友們也可以根據實際的業務需要進行相應的擴展。
擴展使用Spring EL表達式
假設現在內部有下面這樣一個接口,其中有一個query方法,接收一個參數type。這里我們簡化一點,假設只要接收這么一個參數,然后對應不同的取值時將返回不同的結果。
public interface RealService { Object query(int type); }
這個接口是對外開放的,通過對應的URL可以請求到該方法,我們定義了對應的Controller方法如下:
@RequestMapping("/service/{type}") public Object query(@PathVariable("type") int type) { return this.realService.query(type); }
上面的接口服務在進行查詢的時候針對type是有權限的,不是每個用戶都可以使用每種type進行查詢的,需要擁有對應的權限才行。所以針對上面的處理器方法我們需要加上權限控制,而且在控制時需要的權限是隨著參數type動態變的。假設關于type的每項權限的定義是query:type的形式,比如type=1時需要的權限是query:1,type=2時需要的權限是query:2。在沒有與Spring整合時,我們會如下這樣做:
@RequestMapping("/service/{type}") public Object query(@PathVariable("type") int type) { SecurityUtils.getSubject().checkPermission("query:" + type); return this.realService.query(type); }
但是與Spring整合后,上面的做法耦合性強,我們會更希望通過整合后的注解來進行權限控制。對于上面的場景我們更希望通過@RequiresPermissions來指定需要的權限,但是@RequiresPermissions中定義的權限是靜態文本,固定的。它沒法滿足我們動態的需求。這個時候可能你會想著我們可以把Controller處理方法拆分為多個,單獨進行權限控制。比如下面這樣:
@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的取值范圍比較小的時候還可以,但是如果像上面這樣可能的取值有200種,把它們窮舉出來定義單獨的處理器方法并進行權限控制就顯得有點麻煩了。另外就是如果將來type的取值有變動,我們還得添加新的處理器方法。所以最好的辦法是讓@RequiresPermissions支持動態的權限定義,同時又可以維持靜態定義的支持。通過前面的分析我們知道,切入點是PermissionAnnotationHandler,而它里面是沒有提供對權限校驗的擴展的。我們如果想對它擴展簡單的辦法就是把它整體的替換。但是我們需要動態處理的權限是跟方法參數相關的,而PermissionAnnotationHandler中是取不到方法參數的,為此我們不能直接替換掉PermissionAnnotationHandler。PermissionAnnotationHandler是由PermissionAnnotationMethodInterceptor調用的,在其父類AuthorizingAnnotationMethodInterceptor的assertAuthorized方法中調用PermissionAnnotationHandler時是可以獲取到方法參數的。為此我們的擴展點就選在PermissionAnnotationMethodInterceptor類上,我們也需要把它整體的替換。Spring的EL表達式可以支持解析方法參數值,這里我們選擇引入Spring的EL表達式,在@RequiresPermissions定義權限時可以使用Spring EL表達式引入方法參數。同時為了兼顧靜態的文本。這里引入Spring的EL表達式模板。關于Spring的EL表達式模板可以參考筆者的這篇博文。我們定義自己的PermissionAnnotationMethodInterceptor,把它繼承自PermissionAnnotationMethodInterceptor,重寫assertAuthoried方法,方法的實現邏輯參考PermissionAnnotationHandler中的邏輯,但是所使用的@RequiresPermissions中的權限定義,是我們使用Spring EL表達式基于當前調用的方法作為EvaluationContext解析后的結果。以下是我們自己定義的PermissionAnnotationMethodInterceptor實現。
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表達式解析后的權限定義替換原來的權限定義 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后,我們需要替換原來的PermissionAnnotationMethodInterceptor為我們自己的PermissionAnnotationMethodInterceptor。根據前面介紹的Shiro整合Spring后使用@RequiresPermissions等注解的原理我們知道PermissionAnnotationMethodInterceptor是由AopAllianceAnnotationsAuthorizingMethodInterceptor指定的,而后者又是由AuthorizationAttributeSourceAdvisor指定的。為此我們需要在定義AuthorizationAttributeSourceAdvisor時通過顯示定義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"/>
為了演示前面示例的動態的權限,我們把角色與權限的關系調整如下,讓role1、role2和role3分別擁有query:1、query:2和query:3的權限。此時user1將擁有query:1和query:2的權限。
<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>
此時@RequiresPermissions中指定權限時就可以使用Spring EL表達式支持的語法了。因為我們在定義SelfPermissionAnnotationMethodInterceptor時已經指定了應用基于模板的表達式解析,此時權限中定義的文本都將作為文本解析,動態的部分默認需要使用#{前綴和}后綴包起來(這個前綴和后綴是可以指定的,但是默認就好)。在動態部分中可以使用#前綴引用變量,基于方法的表達式解析中可以使用參數名或p參數索引的形式引用方法參數。所以上面我們需要動態的權限的query方法的@RequiresPermissions定義如下。
@RequestMapping("/service/{type}") @RequiresPermissions("query:#{#type}") public Object query(@PathVariable("type") int type) { return this.realService.query(type); }
這樣user1在訪問/service/1和/service/2是OK的,但是在訪問/service/3和/service/300時會提示沒有權限,因為user1沒有query:3和query:300的權限。
看完了這篇文章,相信你對“Spring整合Shiro并擴展使用EL表達式的示例分析”有了一定的了解,如果想了解更多相關知識,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。