Spring 整合Shiro 并扩展使用EL表达式的实例详解

Shiro是一个轻量级的权限控制框架,应用非常广泛。本文的重点是介绍Spring整合Shiro,并通过扩展使用Spring的EL表达式,使@RequiresRoles等支持动态的参数。对Shiro的介绍则不在本文的讨论范围之内,读者如果有对shiro不是很了解的,可以通过其官方网站了解相应的信息。infoq上也有一篇文章对shiro介绍比较全面的,也是官方推荐的,其地址是https://www.infoq.com/articles/apache-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表达式的实例详解,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • Spring @Scheduler使用cron表达式时的执行问题详解

    前言 Spring Scheduler里有两个概念:任务(Task)和运行任务的框架(TaskExecutor/TaskScheduler).TaskExecutor顾名思义,是任务的执行器,允许我们异步执行多个任务.TaskScheduler是任务调度器,来运行未来的定时任务.触发器Trigger可以决定定时任务是否该运行了,最常用的触发器是CronTrigger.Spring内置了多种类型的TaskExecutor和TaskScheduler,方便用户根据不同业务场景选择. 本文主要介绍了关

  • 详解Spring 框架中切入点 pointcut 表达式的常用写法

    自从使用 AspectJ 风格切面配置,使得 spring 的切面配置大大简化,但是 AspectJ 是另外一个开源项目,其规则表达式的语法也稍稍有些怪异. 下面给出一些常见示例的写法,例如,下面是一个对 Service 包上所有方法的切面配置: <aop:config> <aop:pointcut id="serviceOperation" expression="execution(* *..service*..*(..))"/> <

  • SpringBoot 调度任务及常用任务表达式

    1.首先需要用@EnableScheduling注解到*applicatin.java,用来检测是否有调度任务. 2.@Scheduled 注解用于标注这个方法是一个定时任务的方法.Spring会自动扫描这个注解,启动调度任务. package com.david.translate.quartz; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.schedulin

  • springboot Quartz动态修改cron表达式的方法

    1.概述: 在开发中有的时候需要去手动禁止和启用定时任务,修改定时任务的cron表达式然后再让其动态生效,之前有过SSM的类似的业务的开发但是忘记写下来了...只好重新温习了一次,加上最近比较流行springBoot所以升级了一下用springBoot来完成. 2.关联技术 SpringBoot.Quartz.H2.thymeleaf (好像就这么多) 3.具体流程 1)首先去手动创建一个调度器工厂对象-SchedulerFactoryBean;其实应该不用手动创建的但是为了顾及到业务的复杂性所

  • Spring spel表达式使用方法示例

    spring in action第三版读书笔记 spring3.0引入了spring expression language(spel)语言,通过spel我们可以实现 1.通过bean的id对bean进行引用 2.调用方法以及引用对象中的属性 3.计算表达式的值 4.正则表达式的匹配 5.集合的操作 spel最终的目标是得到表达式计算之后的值,这些表达式可能是列举的一些值,引用对象的某些属性,或者是类中的某些常量,复杂的spel表达式通常都是由一些简单的元素构成的.最简单的仅仅是得到一些给出元素

  • 浅谈SpringMVC jsp前台获取参数的方式 EL表达式

    JAVA: request.setAttribute("msg", "1234"); session.setAttribute("msg2", "1234"); JSP: ${requestScope.msg} ${sessionScope.msg2} JAVA: ModelAndView ModelMap Model里添加的参数 JSP: 直接用${参数名} JAVA: 前台表单里的信息,或者是直接在url后面以?name=

  • SpringMVC中Model和ModelAndView的EL表达式取值方法

    model和modelMap(spring 封装),Java.util.Map ModelMap(视图) ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("name", "xxx"); modelAndView.setViewName("/user/index"); return modelAndView; //对于ModelAndView构造函数可以指

  • Spring组件开发模式支持SPEL表达式

    本文是一个 Spring 扩展支持 SPEL 的简单模式,方便第三方通过 Spring 提供额外功能. 简化版方式 这种方式可以在任何能获取ApplicationContext 的地方使用.还可以提取一个方法处理动态 SPEL 表达式. import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; import org.springframework.beans.

  • Spring 整合Shiro 并扩展使用EL表达式的实例详解

    Shiro是一个轻量级的权限控制框架,应用非常广泛.本文的重点是介绍Spring整合Shiro,并通过扩展使用Spring的EL表达式,使@RequiresRoles等支持动态的参数.对Shiro的介绍则不在本文的讨论范围之内,读者如果有对shiro不是很了解的,可以通过其官方网站了解相应的信息.infoq上也有一篇文章对shiro介绍比较全面的,也是官方推荐的,其地址是https://www.infoq.com/articles/apache-shiro. Shiro整合Spring 首先需要

  • JSP中EL表达式的用法详解(必看篇)

    EL 全名为Expression Language EL 语法很简单,它最大的特点就是使用上很方便.接下来介绍EL主要的语法结构: ${sessionScope.user.sex} 所有EL都是以${为起始.以}为结尾的.上述EL范例的意思是:从Session的范围中,取得 用户的性别.假若依照之前JSP Scriptlet的写法如下: User user =(User)session.getAttribute("user"); String sex =user.getSex( );

  • FasfDFS整合Java实现文件上传下载功能实例详解

    在上篇文章给大家介绍了FastDFS安装和配置整合Nginx-1.13.3的方法,大家可以点击查看下. 今天使用Java代码实现文件的上传和下载.对此作者提供了Java API支持,下载fastdfs-client-java将源码添加到项目中.或者在Maven项目pom.xml文件中添加依赖 <dependency> <groupId>org.csource</groupId> <artifactId>fastdfs-client-java</arti

  • springboot整合mybatis将sql打印到日志的实例详解

    在前台请求数据的时候,sql语句一直都是打印到控制台的,有一个想法就是想让它打印到日志里,该如何做呢? 见下面的mybatis配置文件: <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-

  • Spring加载properties文件的两种方式实例详解

    在项目中如果有些参数经常需要修改,或者后期可能需要修改,那我们最好把这些参数放到properties文件中,源代码中读取properties里面的配置,这样后期只需要改动properties文件即可,不需要修改源代码,这样更加方便.在Spring中也可以这么做,而且Spring有两种加载properties文件的方式:基于xml方式和基于注解方式.下面分别讨论下这两种方式. 1. 通过xml方式加载properties文件 我们以Spring实例化dataSource为例,我们一般会在beans

  • Spring依赖注入的两种方式(根据实例详解)

    1,Set注入    2,构造注入 Set方法注入: 原理:通过类的setter方法完成依赖关系的设置 name属性的取值依setter方法名而定,要求这个类里面这个对应的属性必须有setter方法. Set方法注入时spring中配置文件: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans&qu

  • Spring Boot的filter(过滤器)简单使用实例详解

    过滤器(Filter)的注册方法和 Servlet 一样,有两种方式:代码注册或者注解注册 1.代码注册方式 通过代码方式注入过滤器 @Bean public FilterRegistrationBean indexFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(new IndexFilter()); registration.addUrlPatterns("/&quo

  • Spring Boot 2 实战:自定义启动运行逻辑实例详解

    本文实例讲述了Spring Boot 2 实战:自定义启动运行逻辑.分享给大家供大家参考,具体如下: 1. 前言 不知道你有没有接到这种需求,项目启动后立马执行一些逻辑.比如缓存预热,或者上线后的广播之类等等.可能现在没有但是将来会有的.想想你可能的操作, 写个接口上线我调一次行吗?NO!NO!NO!这种初级菜鸟才干的事.今天告诉你个骚操作使得你的代码更加优雅,逼格更高. 2. CommandLineRunner 接口 package org.springframework.boot; impo

  • spring boot+mybatis搭建一个后端restfull服务的实例详解

    1.创建一个maven项目. 2.在pom.xml中引入依赖包,如下所示: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="

  • Spring Boot 集成 Mybatis Plus 自动填充字段的实例详解

    一般在表设计的时候,都会在表中添加一些系统字段,比如 create_time.update_time等. 阿里巴巴开发手册中也有这样的提示,如果对于这些公共字段可以进行统一处理,不需要每次进行插入或者更新操作的时候 set 一下,就可以提高开发效率,解放双手. 加入依赖 下面就通过 MyBatis Plus 来完成字段自动填充,首先加入 MyBatis Plus 依赖: <dependency>     <groupId>com.baomidou</groupId>  

随机推荐