欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Spring 整合Shiro 并擴展使用EL表達式的實例詳解

 更新時間:2018年03月16日 15:53:32   作者:Elim的博客  
Shiro是一個輕量級的權(quán)限控制框架,應(yīng)用非常廣泛。本文的重點是介紹Spring整合Shiro,并通過擴展使用Spring的EL表達式。需要的朋友可以參考下

Shiro是一個輕量級的權(quán)限控制框架,應(yīng)用非常廣泛。本文的重點是介紹Spring整合Shiro,并通過擴展使用Spring的EL表達式,使@RequiresRoles等支持動態(tài)的參數(shù)。對Shiro的介紹則不在本文的討論范圍之內(nèi),讀者如果有對shiro不是很了解的,可以通過其官方網(wǎng)站了解相應(yīng)的信息。infoq上也有一篇文章對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>

接下來需要在你的web.xml中定義一個shiroFilter,應(yīng)用它來攔截所有的需要權(quán)限控制的請求,通常是配置為/*。另外該Filter需要加入最前面,以確保請求進來后最先通過shiro的權(quán)限控制。這里的Filter對應(yīng)的class配置的是DelegatingFilterProxy,這是Spring提供的一個Filter的代理,可以使用Spring bean容器中的一個bean來作為當(dāng)前的Filter實例,對應(yīng)的bean就會取filter-name對應(yīng)的那個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,其會產(chǎn)生一個AbstractShiroFilter類型的bean。通過ShiroFilterFactoryBean我們可以指定一個SecurityManager,這里使用的DefaultWebSecurityManager需要指定一個Realm,如果需要指定多個Realm則通過realms指定。這里簡單起見就直接使用基于文本定義的TextConfigurationRealm。通過loginUrl指定登錄地址、successUrl指定登錄成功后需要跳轉(zhuǎn)的地址,unauthorizedUrl指定權(quán)限不足時的提示頁面。filterChainDefinitions則定義URL與需要使用的Filter之間的關(guān)系,等號右邊的是filter的別名,默認(rèn)的別名都定義在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
   # 其它地址都要求用戶已經(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"/>
<!-- 簡單起見,這里就使用基于文本的Realm實現(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,則可以通過ShiroFilterFactoryBean的filters指定自定義的Filter及其別名映射關(guān)系。比如下面這樣我們新增了一個別名為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>

其實我們需要應(yīng)用的Filter別名定義也可以不直接通過ShiroFilterFactoryBean的setFilters()來指定,而是直接在對應(yīng)的bean容器中定義對應(yīng)的Filter對應(yīng)的bean。因為默認(rèn)情況下,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
   # 其它地址都要求用戶已經(jīng)登錄了
   /** = authc,logger
  </value>
 </property>
</bean>
<bean id="logger" class="com.elim.chat.shiro.filter.LoggerFilter"/>

經(jīng)過以上幾步,Shiro和Spring的整合就完成了,這個時候我們請求工程的任意路徑都會要求我們登錄,且會自動跳轉(zhuǎn)到loginUrl指定的路徑讓我們輸入用戶名/密碼登錄。這個時候我們應(yīng)該提供一個表單,通過username獲得用戶名,通過password獲得密碼,然后提交登錄請求的時候請求需要提交到loginUrl指定的地址,但是請求方式需要變?yōu)镻OST。登錄時使用的用戶名/密碼是我們在TextConfigurationRealm中定義的用戶名/密碼,基于我們上面的配置則可以使用user1/pass1、admin/admin等。登錄成功后就會跳轉(zhuǎn)到successUrl參數(shù)指定的地址了。如果我們是使用user1/pass1登錄的,則我們還可以試著訪問一下/admin/index,這個時候會因為權(quán)限不足跳轉(zhuǎn)到unauthorized.jsp。

啟用基于注解的支持

基本的整合需要我們把URL需要應(yīng)用的權(quán)限控制都定義在ShiroFilterFactoryBean的filterChainDefinitions中。這有時候會沒那么靈活。Shiro為我們提供了整合Spring后可以使用的注解,它允許我們在需要進行權(quán)限控制的Class或Method上加上對應(yīng)的注解以定義訪問Class或Method需要的權(quán)限,如果是定義中Class上的,則表示調(diào)用該Class中所有的方法都需要對應(yīng)的權(quán)限(注意需要是外部調(diào)用,這是動態(tài)代理的局限)。要使用這些注解我們需要在Spring的bean容器中添加下面兩個bean定義,這樣才能在運行時根據(jù)注解定義來判斷用戶是否擁有對應(yīng)的權(quán)限。這是通過Spring的AOP機制來實現(xiàn)的,關(guān)于Spring Aop如果有不是特別了解的,可以參考筆者寫在iteye的《Spring Aop介紹專欄》。下面的兩個bean定義,AuthorizationAttributeSourceAdvisor是定義了一個Advisor,其會基于Shiro提供的注解配置的方法進行攔截,校驗權(quán)限。DefaultAdvisorAutoProxyCreator則是提供了為標(biāo)注有Shiro提供的權(quán)限控制注解的Class創(chuàng)建代理對象,并在攔截到目標(biāo)方法調(diào)用時應(yīng)用AuthorizationAttributeSourceAdvisor的功能。當(dāng)攔截到了用戶的一個請求,而該用戶沒有對應(yīng)方法或類上標(biāo)注的權(quán)限時,將拋出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。因為前面兩種情況都會自動添加與DefaultAdvisorAutoProxyCreator類似的bean。關(guān)于DefaultAdvisorAutoProxyCreator的更多介紹也可以參考筆者的Spring Aop自動創(chuàng)建代理對象的原理這篇博客。

Shiro提供的權(quán)限控制注解如下:

RequiresAuthentication:需要用戶在當(dāng)前會話中是被認(rèn)證過的,即需要通過用戶名/密碼登錄過,不包括RememberMe自動登錄。

RequiresUser:需要用戶是被認(rèn)證過的,可以是在本次會話中通過用戶名/密碼登錄認(rèn)證,也可以是通過RememberMe自動登錄。

RequiresGuest:需要用戶是未登錄的。
RequiresRoles:需要用戶擁有指定的角色。
RequiresPermissions:需要用戶擁有指定的權(quán)限。

前面三個都很好理解,而后面兩個是類似的。筆者這里拿@RequiresPermissions來做個示例。首先我們把上面定義的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可以添加在方法上,用來指定調(diào)用該方法時需要擁有的權(quán)限。下面的代碼我們就指定了在訪問/perm1時必須擁有perm1這個權(quán)限。這個時候user1和user2都能訪問。

@RequestMapping("/perm1")
@RequiresPermissions("perm1")
public Object permission1() {
 return "permission1";
}

如果需要指定必須同時擁有多個權(quán)限才能訪問某個方法,可以把需要指定的權(quán)限以數(shù)組的形式指定(注解上的數(shù)組屬性指定單個的時候可以不加大括號,需要指定多個時就需要加大括號)。比如下面這樣我們就指定了在訪問/perm1AndPerm4時用戶必須同時擁有perm1和perm4這兩個權(quán)限。這時候就只有user2可以訪問,因為只有它才同時擁有perm1和perm4。

@RequestMapping("/perm1AndPerm4")
@RequiresPermissions({"perm1", "perm4"})
public Object perm1AndPerm4() {
 return "perm1AndPerm4";
}

當(dāng)同時指定了多個權(quán)限時,默認(rèn)多個權(quán)限之間的關(guān)系是與的關(guān)系,即需要同時擁有指定的所有的權(quán)限。如果只需要擁有指定的多個權(quán)限中的一個就可以訪問,則我們可以通過logical=Logical.OR指定多個權(quán)限之間是或的關(guān)系。比如下面這樣我們就指定了在訪問/perm1OrPerm4時只需要擁有perm1或perm4權(quán)限即可,這樣user1和user2都可以訪問該方法。

@RequestMapping("/perm1OrPerm4")
@RequiresPermissions(value={"perm1", "perm4"}, logical=Logical.OR)
public Object perm1OrPerm4() {
 return "perm1OrPerm4";
}

@RequiresPermissions也可以標(biāo)注在Class上,表示在外部訪問Class中的方法時都需要有對應(yīng)的權(quán)限。比如下面這樣我們在Class級別指定了需要擁有權(quán)限perm2,而在index()方法上則沒有指定需要任何權(quán)限,但是我們在訪問該方法時還是需要擁有Class級別指定的權(quán)限。此時將只有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;
 }
}

當(dāng)Class和方法級別都同時擁有@RequiresPermissions時,方法級別的擁有更高的優(yōu)先級,而且此時將只會校驗方法級別要求的權(quán)限。如下我們在Class級別指定了需要perm2權(quán)限,而在方法級別指定了需要perm3權(quán)限,那么在訪問/foo時將只需要擁有perm3權(quán)限即可訪問到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權(quán)限。因為RequiresRoles和RequiresPermissions屬于不同維度的權(quán)限定義,Shiro在校驗的時候都將校驗一遍,但是如果Class和方法上都擁有同類型的權(quán)限控制定義的注解時,則只會以方法上的定義為準(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)限控制注解的用法也是類似的,其它注解的用法請感興趣的朋友自己實踐。

基于注解控制權(quán)限的原理

上面使用@RequiresPermissions我們指定的權(quán)限都是靜態(tài)的,寫本文的一個主要目的是介紹一種方法,通過擴展實現(xiàn)來使指定的權(quán)限可以是動態(tài)的。但是在擴展前我們得知道它底層的工作方式,即實現(xiàn)原理,我們才能進行擴展。所以接下來我們先來看一下Shiro整合Spring后使用@RequiresPermissions的工作原理。在啟用對@RequiresPermissions的支持時我們定義了如下bean,這是一個Advisor,其繼承自StaticMethodMatcherPointcutAdvisor,它的方法匹配邏輯是只要Class或Method上擁有Shiro的幾個權(quán)限控制注解即可,而攔截以后的處理邏輯則是由相應(yīng)的Advice指定。

<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
 <property name="securityManager" ref="securityManager"/>
</bean>

以下是AuthorizationAttributeSourceAdvisor的源碼。我們可以看到在其構(gòu)造方法中通過setAdvice()指定了AopAllianceAnnotationsAuthorizingMethodInterceptor這個Advice實現(xiàn)類,這是基于MethodInterceptor的實現(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的源碼如下。其實現(xiàn)的MethodInterceptor接口的invoke方法又調(diào)用了父類的invoke方法。同時我們要看到在其構(gòu)造方法中創(chuàng)建了一些AuthorizingAnnotationMethodInterceptor實現(xiàn),這些實現(xiàn)才是實現(xiàn)權(quán)限控制的核心,待會我們會挑出PermissionAnnotationMethodInterceptor實現(xiàn)類來看其具體的實現(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);
 }
}

通過看父類的invoke方法實現(xiàn),最終我們會看到核心邏輯是調(diào)用assertAuthorized方法,而該方法的實現(xiàn)(源碼如下)又是依次判斷配置的AuthorizingAnnotationMethodInterceptor是否支持當(dāng)前方法進行權(quán)限校驗(通過判斷Class或Method上是否擁有其支持的注解),當(dāng)支持時則會調(diào)用其assertAuthorized方法進行權(quán)限校驗,而AuthorizingAnnotationMethodInterceptor又會調(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);
   }
  }
 }
}

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

public class PermissionAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {
 public PermissionAnnotationMethodInterceptor() {
  super( new PermissionAnnotationHandler() );
 }
 public PermissionAnnotationMethodInterceptor(AnnotationResolver resolver) {
  super( new PermissionAnnotationHandler(), resolver);
 }
}

接下來我們來看PermissionAnnotationHandler的assertAuthorized方法實現(xiàn),其完整代碼如下。從實現(xiàn)上我們可以看到其會從Annotation中獲取配置的權(quán)限值,而這里的Annotation就是RequiresPermissions注解。而且在進行權(quán)限校驗時都是直接使用的我們定義注解時指定的文本值,待會我們進行擴展時就將從這里入手。

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方法參數(shù)的Annotation是由AuthorizingAnnotationMethodInterceptor在調(diào)用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方法實現(xiàn),其實現(xiàn)如下。從下面的代碼可以看到,其在尋找注解時是優(yōu)先尋找Method上的,如果在Method上沒有找到會從當(dāng)前方法調(diào)用的所屬Class上尋找對應(yīng)的注解。從這里也可以看到為什么我們之前在Class和Method上都定義了相同類型的權(quán)限控制注解時生效的是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后支持的權(quán)限控制注解的原理已經(jīng)有了比較深入的理解。上面貼出的源碼只是部分筆者認(rèn)為比較核心的,有想詳細了解完整內(nèi)容的請讀者自己沿著筆者提到的思路去閱讀完整代碼。
了解了這塊基于注解進行權(quán)限控制的原理后,讀者朋友們也可以根據(jù)實際的業(yè)務(wù)需要進行相應(yīng)的擴展。

擴展使用Spring EL表達式

假設(shè)現(xiàn)在內(nèi)部有下面這樣一個接口,其中有一個query方法,接收一個參數(shù)type。這里我們簡化一點,假設(shè)只要接收這么一個參數(shù),然后對應(yīng)不同的取值時將返回不同的結(jié)果。

public interface RealService {
 Object query(int type);
 
}

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

@RequestMapping("/service/{type}")
public Object query(@PathVariable("type") int type) {
 return this.realService.query(type);
}

上面的接口服務(wù)在進行查詢的時候針對type是有權(quán)限的,不是每個用戶都可以使用每種type進行查詢的,需要擁有對應(yīng)的權(quán)限才行。所以針對上面的處理器方法我們需要加上權(quán)限控制,而且在控制時需要的權(quán)限是隨著參數(shù)type動態(tài)變的。假設(shè)關(guān)于type的每項權(quán)限的定義是query:type的形式,比如type=1時需要的權(quán)限是query:1,type=2時需要的權(quán)限是query:2。在沒有與Spring整合時,我們會如下這樣做:

@RequestMapping("/service/{type}")
public Object query(@PathVariable("type") int type) {
 SecurityUtils.getSubject().checkPermission("query:" + type);
 return this.realService.query(type);
}

但是與Spring整合后,上面的做法耦合性強,我們會更希望通過整合后的注解來進行權(quán)限控制。對于上面的場景我們更希望通過@RequiresPermissions來指定需要的權(quán)限,但是@RequiresPermissions中定義的權(quán)限是靜態(tài)文本,固定的。它沒法滿足我們動態(tài)的需求。這個時候可能你會想著我們可以把Controller處理方法拆分為多個,單獨進行權(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的取值范圍比較小的時候還可以,但是如果像上面這樣可能的取值有200種,把它們窮舉出來定義單獨的處理器方法并進行權(quán)限控制就顯得有點麻煩了。另外就是如果將來type的取值有變動,我們還得添加新的處理器方法。所以最好的辦法是讓@RequiresPermissions支持動態(tài)的權(quán)限定義,同時又可以維持靜態(tài)定義的支持。通過前面的分析我們知道,切入點是PermissionAnnotationHandler,而它里面是沒有提供對權(quán)限校驗的擴展的。我們?nèi)绻雽λ鼣U展簡單的辦法就是把它整體的替換。但是我們需要動態(tài)處理的權(quán)限是跟方法參數(shù)相關(guān)的,而PermissionAnnotationHandler中是取不到方法參數(shù)的,為此我們不能直接替換掉PermissionAnnotationHandler。PermissionAnnotationHandler是由PermissionAnnotationMethodInterceptor調(diào)用的,在其父類AuthorizingAnnotationMethodInterceptor的assertAuthorized方法中調(diào)用PermissionAnnotationHandler時是可以獲取到方法參數(shù)的。為此我們的擴展點就選在PermissionAnnotationMethodInterceptor類上,我們也需要把它整體的替換。Spring的EL表達式可以支持解析方法參數(shù)值,這里我們選擇引入Spring的EL表達式,在@RequiresPermissions定義權(quán)限時可以使用Spring EL表達式引入方法參數(shù)。同時為了兼顧靜態(tài)的文本。這里引入Spring的EL表達式模板。關(guān)于Spring的EL表達式模板可以參考筆者的這篇博文。我們定義自己的PermissionAnnotationMethodInterceptor,把它繼承自PermissionAnnotationMethodInterceptor,重寫assertAuthoried方法,方法的實現(xiàn)邏輯參考PermissionAnnotationHandler中的邏輯,但是所使用的@RequiresPermissions中的權(quán)限定義,是我們使用Spring EL表達式基于當(dāng)前調(diào)用的方法作為EvaluationContext解析后的結(jié)果。以下是我們自己定義的PermissionAnnotationMethodInterceptor實現(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表達式解析后的權(quán)限定義替換原來的權(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后,我們需要替換原來的PermissionAnnotationMethodInterceptor為我們自己的PermissionAnnotationMethodInterceptor。根據(jù)前面介紹的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"/>

為了演示前面示例的動態(tài)的權(quán)限,我們把角色與權(quán)限的關(guān)系調(diào)整如下,讓role1、role2和role3分別擁有query:1、query:2和query:3的權(quán)限。此時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>

此時@RequiresPermissions中指定權(quán)限時就可以使用Spring EL表達式支持的語法了。因為我們在定義SelfPermissionAnnotationMethodInterceptor時已經(jīng)指定了應(yīng)用基于模板的表達式解析,此時權(quán)限中定義的文本都將作為文本解析,動態(tài)的部分默認(rèn)需要使用#{前綴和}后綴包起來(這個前綴和后綴是可以指定的,但是默認(rèn)就好)。在動態(tài)部分中可以使用#前綴引用變量,基于方法的表達式解析中可以使用參數(shù)名或p參數(shù)索引的形式引用方法參數(shù)。所以上面我們需要動態(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在訪問/service/1和/service/2是OK的,但是在訪問/service/3和/service/300時會提示沒有權(quán)限,因為user1沒有query:3和query:300的權(quán)限。

總結(jié)

以上所述是小編給大家介紹的Spring 整合Shiro 并擴展使用EL表達式的實例詳解,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!

相關(guān)文章

  • Java日常練習(xí)題,每天進步一點點(28)

    Java日常練習(xí)題,每天進步一點點(28)

    下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧,希望可以幫到你
    2021-07-07
  • JDK8?HashMap擴容算法demo

    JDK8?HashMap擴容算法demo

    這篇文章主要為大家介紹了JDK8?HashMap擴容算法demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-07-07
  • SpringCloud Gateway使用redis實現(xiàn)動態(tài)路由的方法

    SpringCloud Gateway使用redis實現(xiàn)動態(tài)路由的方法

    這篇文章主要介紹了SpringCloud Gateway使用redis實現(xiàn)動態(tài)路由的方法,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-01-01
  • JAVA 中解密RSA算法JS加密實例詳解

    JAVA 中解密RSA算法JS加密實例詳解

    這篇文章主要介紹了JAVA 中解密RSA算法JS加密 的相關(guān)資料,需要的朋友可以參考下
    2017-04-04
  • java實現(xiàn)圖片上插入文字并保存

    java實現(xiàn)圖片上插入文字并保存

    這篇文章主要為大家詳細介紹了java實現(xiàn)圖片上插入文字并保存,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-06-06
  • springboot+zookeeper實現(xiàn)分布式鎖的示例代碼

    springboot+zookeeper實現(xiàn)分布式鎖的示例代碼

    本文主要介紹了springboot+zookeeper實現(xiàn)分布式鎖的示例代碼,文中根據(jù)實例編碼詳細介紹的十分詳盡,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • Java?Validated?分組校驗的使用

    Java?Validated?分組校驗的使用

    這篇文章主要介紹了Java?Validated?分組校驗的使用,文章記錄所以在這里記錄下分組校驗注解@Validated的使用,具有一定的參考價值,需要的朋友可以參考一下
    2022-02-02
  • 淺談java中的一維數(shù)組、二維數(shù)組、三維數(shù)組、多維數(shù)組

    淺談java中的一維數(shù)組、二維數(shù)組、三維數(shù)組、多維數(shù)組

    下面小編就為大家?guī)硪黄獪\談java中的一維數(shù)組、二維數(shù)組、三維數(shù)組、多維數(shù)組。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-05-05
  • java單元測試JUnit框架原理與用法實例教程

    java單元測試JUnit框架原理與用法實例教程

    這篇文章主要介紹了java單元測試JUnit框架原理與用法,結(jié)合實例形式較為詳細的分析了java單元測試JUnit框架的概念、原理、使用方法及相關(guān)注意事項,需要的朋友可以參考下
    2017-11-11
  • Spring?Boot快速過濾出一次請求的所有日志

    Spring?Boot快速過濾出一次請求的所有日志

    這篇文章主要介紹了Spring?Boot快速過濾出一次請求的所有日志,本文講述了如何使用MDC工具來快速過濾一次請求的所有日志,并通過裝飾器模式使得MDC工具在異步線程里也能生效,需要的朋友可以參考下
    2022-11-11

最新評論