spring对shiro注解支持的原理

来源:互联网 发布:mac qq接受文件没反应 编辑:程序博客网 时间:2024/06/07 09:35


背景介绍:

       笔者最近要开发一个开放的管理后台,既然是给大量的用户做的,就必须要考虑到用户的权限问题,做到安全的管理,笔者以前用过的是spring security(简称ss),

但是根据网上的资料和同事的推荐,发现使用shiro来做权限控制比ss更好一些,尤其是spring组织的项目也是用的shiro,没有用自家的ss,所以笔者准备入shiro坑


本篇文章主要讲的是spring对shiro注解的支持和扩展,至于spring集成shiro注解的部分就不做详细说明。


环境介绍:

spring:4.1.6

shiro:1.2.3

shiro-spring:1.2.3


示例代码


@Controller@RequestMapping(value="demo")@RequiresRoles("ADMIN")public class DemoController extends BaseController{/*----------------角色校验demo--------------------*//** * 角色测试一:代码方式验证角色 * @return */@RequestMapping(value="role/demo01")@ResponseBodypublic String demo01(){//判断有没有admin的角色SecurityUtils.getSubject().checkRole("admin");System.out.println("测试代码方式验证角色");return "demo01";}/** * 角色测试二:注解方式验证单个角色 * @return */@RequiresRoles("USER")@RequestMapping(value="role/demo02")@ResponseBodypublic String demo02(){System.out.println("测试注解方式验证单个角色");return "demo02";}/** * 角色测试三:注解方式验证多个角色 * @return */@RequiresRoles(logical=Logical.OR,value={"USER","ADMIN"})@RequestMapping(value="role/demo03")@ResponseBodypublic String demo03(){System.out.println("测试注解方式验证多个角色");return "demo03";}/** * 角色测试四:controller类上的注解方式验证角色(结果:不支持,只支持方法级别的***最后自定义实现了) * @return */@RequestMapping(value="role/demo04")@ResponseBodypublic String demo04(){System.out.println("controller类上的注解方式验证角色");return "demo04";}}




*spring对shiro注解的支持扩展支持类级别

这个是什么意思呢?说的是shiro-spring包支持的@RequiresPermissions, @RequiresRoles,@RequiresUser, @RequiresGuest, @RequiresAuthentication这5个注解,

只有在方法上使用的时候才会生效,但是查看注解的代码,以@RequiresRoles为例,注解应该在方法上,类上都应该有效的:

@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface RequiresPermissions {    /**     * The permission string which will be passed to {@link org.apache.shiro.subject.Subject#isPermitted(String)}     * to determine if the user is allowed to invoke the code protected by this annotation.     */    String[] value();        /**     * The logical operation for the permission checks in case multiple roles are specified. AND is the default     * @since 1.1.0     */    Logical logical() default Logical.AND; }


但笔者在例子测试的时候,发现  demo02() 上的@RequiresRoles(“user”)是可以起作用的,但是在测试demo04()的时候,类DemoController上的@RequiresRoles(“admin”)是不起作用的,所以现在问题来了:

笔者的目的:让@RequiresRoles在方法上和类上都可以生效

应用场景:@RequiresRoles在方法上的效果自然就是控制访问这个路径的权限,@RequiresRoles在类上的作用什么呢?@RequiresRoles在类上的作用就是起到默认的权限控制,

也就是说像demo04()这样没有指定@RequiresRoles的方法,默认需要的权限是DemoController类上指定的@RequiresRoles权限,但是向demo01()已经指定权限的方法,就会覆盖类上的权限设置

方法(细粒度)权限    会覆盖     类(粗粒度)权限

有人会想,这样会有什么用呢,我举个例子,比如说一个小说的模块所有的url访问都需要USER权限,如果只是一个个的方法都设置成一样的@RequiresRoles权限,是不是很麻烦,但是如果支持了类上@RequiresRoles(默认权限)

就会让所有没有指定@RequiresRoles权限的方法默认是需要类上的@RequiresRoles指定的权限

@RequiresRoles现在已经解释清除了,那么现在shiro-spring是不支持类上@RequiresRoles的,我们应该怎么让它支持,满足我们的需求呢?

笔者查看了一下shiro-spring扫描@RequiresRoles权限的源码:

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;    /**     * Create a new AuthorizationAttributeSourceAdvisor.     */    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());                if ( isAuthzAnnotationPresent(m) ) {                    return true;                }            } 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(Method method) {        for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {            Annotation a = AnnotationUtils.findAnnotation(method, annClass);            if ( a != null ) {                return true;            }        }        return false;    }}

这个扫描shiro注解类的源码最重要的方法时matches方法,会通过返回true的方式来判断某个方法是否带有shiro权限注解,是的,只会判断方法,那么我们现在需要支持类上的权限注解,怎么办呢?

笔者把源码给修改了一下:


/** * 自定义的注解权限AOP扫描 * @author zhihua * */public class OpenCmsAuthorizationAdvisor extends AuthorizationAttributeSourceAdvisor{//权限注解private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =            new Class[] {                    RequiresPermissions.class, RequiresRoles.class,                    RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class            };//web注解private static final Class<? extends Annotation>[] WEB_ANNOTATION_CLASSES =            new Class[] {                    RequestMapping.class            };/**     * Create a new AuthorizationAttributeSourceAdvisor.     */    public OpenCmsAuthorizationAdvisor() {        setAdvice(new OpenCmsAnnotationsAuthorizingMethodInterceptor());    }/** * 匹配带有注解的方法 */@Overridepublic boolean matches(Method method, Class targetClass) {boolean flag = super.matches(method, targetClass);//如果方法上没有权限注解,尝试获取类上的默认权限注解if(!flag && isAuthzAnnotationPresent(targetClass) && isWebAnnotationPresent(method)){flag = true;}return flag;}private boolean isAuthzAnnotationPresent(Class<BaseController> clazz) {        for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {            Annotation a = AnnotationUtils.findAnnotation(clazz, 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;    }private boolean isWebAnnotationPresent(Method method) {        for( Class<? extends Annotation> annClass : WEB_ANNOTATION_CLASSES ) {            Annotation a = AnnotationUtils.findAnnotation(method, annClass);            if ( a != null ) {                return true;            }        }        return false;    }}



添加了一个逻辑,就是如果方法上没有发现shiro权限注解的话,会先判断方法是否有requestmapping注解,判断是否是一个接口,这样是因为要屏蔽equals()等方法,如果是一个接口,就会扫描类上的@RequiresRoles作为方法

的指定访问权限,这样就实现了让类上的shiro注解生效,作为默认的权限注解。但是有的读者可能会问,这里只是扫描,那对类上的@RequiresRoles注解进行处理的地方是怎样的原理呢?在这里,我要跟大家说,对类上的

@RequiresRoles注解进行的相关处理shiro-spring.jar是支持的,只是在扫描的时候不支持,所以笔者在这里怀疑这是shiro-spring.jar的一个bug.然后关于对类上的@RequiresRoles注解进行处理的地方的原理讲解在下面会详细说明,

这里暂时跳过


对了,做了扩展以后,spring的配置文件也要做修改

<!-- Enable Shiro Annotations for Spring-configured beans.  Only run after --><!-- the lifecycleBeanProcessor has run: --><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 class="com.boluofan.opencms.auth.OpenCmsAuthorizationAdvisor">    <property name="securityManager" ref="securityManager"/></bean>




*对类上的@RequiresRoles注解进行处理的原理

其实这个很简单,我简单的贴出源码给大家看一下就知道了,还是以@requireRoles为例子

DefaultAnnotationResolver:

public Annotation getAnnotation(MethodInvocation mi, Class<? extends Annotation> clazz) {        if (mi == null) {            throw new IllegalArgumentException("method argument cannot be null");        }        Method m = mi.getMethod();        if (m == null) {            String msg = MethodInvocation.class.getName() + " parameter incorrectly constructed.  getMethod() returned null";            throw new IllegalArgumentException(msg);        }        Annotation annotation = m.getAnnotation(clazz);        return annotation == null ? mi.getThis().getClass().getAnnotation(clazz) : annotation;    }


Annotation annotation = m.getAnnotation(clazz);

这行代码是查找方法上的注解,

annotation == null ? mi.getThis().getClass().getAnnotation(clazz) : annotation

这行代码是如果在方法上找不到权限注解,就从类上获取权限注解


然后会跳转到annotation的对应处理方法,

reqireRoles  --> RoleAnnotationHandler

会在RoleAnnotationHandler的assertAuthorized方法里进行权限认证,assertAuthorized方法的权限认证实际上是在AuthorizingRealm.java里边通过

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)

进行校验的,相信大家都这个方法都很熟悉了。这就是对注解的运行原理,如果大家对这部分还是不是很清楚的话,接下来,我会带大家实现一个自定义的shiro-spring权限注解,

shiro-spring.jar只支持@RequiresPermissions, @RequiresRoles,@RequiresUser, @RequiresGuest, @RequiresAuthentication这5个注解,我会带大家实现一个基于jsr的@RolesAllowed注解,同样也是用于角色的认证



*实现自定义shiro权限注解

这个部分笔者会带大家实现一个自定义的shiro注解,用于角色校验,和@RequiresRoles的作用是一样的

首先说一下,spring-shiro注解实现认证的内部原理:

#tomcat在启动的时候shiro-spring中AopAllianceAnnotationsAuthorizingMethodInterceptor注册注解拦截器

#tomcat启动的时候AuthorizationAttributeSourceAdvisor扫描权限注解

#用户的请求经过被注册的注解拦截器拦截


spring-aop
AopAllianceAnnotationsAuthorizingMethodInterceptor.invoke(MethodInvocation methodInvocation)
AuthorizingMethodInterceptor.invoke
AnnotationsAuthorizingMethodInterceptor.assertAuthorized(MethodInvocation methodInvocation)
AuthorizingAnnotationMethodInterceptor.assertAuthorized(MethodInvocation mi) 
RoleAnnotationHandler.assertAuthorized()
AuthorizingRealm.checkRole()

返回验证结果


#注解拦截器经过AuthorizingRealm(可以自定义实现)获取权限


接下来,我们按照上述的机制步骤来实现自定义注解@rolesallowed

#实现自定义shiro权限注解

@Documented@Retention (RUNTIME)@Target({TYPE, METHOD})public @interface RolesAllowed {    String[] value();}


#注册注解拦截器



public class OpenCmsAnnotationsAuthorizingMethodInterceptor extends AopAllianceAnnotationsAuthorizingMethodInterceptor {public OpenCmsAnnotationsAuthorizingMethodInterceptor() {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));        //自定义        interceptors.add(new RoleAllowsAnnotationMethodInterceptor());        setMethodInterceptors(interceptors);}}

rolesallowed注解拦截器

public class RoleAllowsAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {public RoleAllowsAnnotationMethodInterceptor() {super(new RolesAllowedAnnotationHandler());}public RoleAllowsAnnotationMethodInterceptor(AnnotationResolver resolver) {super(new RolesAllowedAnnotationHandler(),resolver);}}

注解处理器
public class RolesAllowedAnnotationHandler extends AuthorizingAnnotationHandler {/** * 构造函数 * @param annotationClass */public RolesAllowedAnnotationHandler() {super(RolesAllowed.class);}@Overridepublic void assertAuthorized(Annotation a) throws AuthorizationException {RolesAllowed rrAnnotation = (RolesAllowed) a;        String[] roles = rrAnnotation.value();getSubject().checkRoles(Arrays.asList(roles));        return;}}



#添加扫描权限注解@rolesallowed

public class OpenCmsAuthorizationAdvisor extends AuthorizationAttributeSourceAdvisor{//权限注解private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =            new Class[] {RolesAllowed.class,                    RequiresPermissions.class, RequiresRoles.class,                    RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class            };//web注解private static final Class<? extends Annotation>[] WEB_ANNOTATION_CLASSES =            new Class[] {                    RequestMapping.class            };/**     * Create a new AuthorizationAttributeSourceAdvisor.     */    public OpenCmsAuthorizationAdvisor() {        setAdvice(new OpenCmsAnnotationsAuthorizingMethodInterceptor());    }/** * 匹配带有注解的方法 */@Overridepublic boolean matches(Method method, Class targetClass) {boolean flag = super.matches(method, targetClass);//如果方法上没有权限注解,尝试获取类上的默认权限注解if(!flag && isAuthzAnnotationPresent(targetClass) && isWebAnnotationPresent(method)){flag = true;}return flag;}/** * 查看BaseController的子类是否有权限注解 * @param clazz * @return */private boolean isAuthzAnnotationPresent(Class<BaseController> clazz) {        for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {            Annotation a = AnnotationUtils.findAnnotation(clazz, annClass);            if ( a != null ) {                return true;            }        }        return false;    }/** * 查看方法上是否有权限注解 * @param method * @return */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;    }/** * 查看方法是否有web注解,是否是一个rest接口 * @param method * @return */private boolean isWebAnnotationPresent(Method method) {        for( Class<? extends Annotation> annClass : WEB_ANNOTATION_CLASSES ) {            Annotation a = AnnotationUtils.findAnnotation(method, annClass);            if ( a != null ) {                return true;            }        }        return false;    }}

#用户的请求经过被注册的注解拦截器拦截

---

#注解拦截器经过DefaultRealm获取权限


public class DefaultRealm extends AuthorizingRealm{/** * 用于角色权限校验 */@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {        String username = (String) principals.fromRealm(getName()).iterator().next();          // 查询用户授权信息  伪代码            SimpleAuthorizationInfo info = 。。。;                          return info;          }}/** * 用于登录校验 */@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {UsernamePasswordToken token = (UsernamePasswordToken) authcToken;          //伪代码            //第二个参数是从数据库中获取到的用户密码(或者密码的MD5),交给shiro去进行校验              return new SimpleAuthenticationInfo(username, user.getPassword(),getName());        }          return null;  }}


关于shiro和spring扫描和注解方式认证权限的过程就到这里了,当然我只是说明了部分原理,大家如果有我没有讲到的地方或者是更好的资料,希望能够分享给我,一起学习进步




2 0