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时用户必须同时拥有perm1perm4这两个权限。这时候就只有user2可以访问,因为只有它才同时拥有perm1perm4

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

当同时指定了多个权限时,默认多个权限之间的关系是与的关系,即需要同时拥有指定的所有的权限。如果只需要拥有指定的多个权限中的一个就可以访问,则我们可以通过logical=Logical.OR指定多个权限之间是或的关系。比如下面这样我们就指定了在访问/perm1OrPerm4时只需要拥有perm1perm4权限即可,这样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权限。因为RequiresRolesRequiresPermissions属于不同维度的权限定义,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:3query:300的权限。

上一篇:SpringMVC路径匹配中使用通配符


下一篇:学习Hadoop大数据基础框架