天天看点

spring对shiro注解支持的原理

背景介绍:

       笔者最近要开发一个开放的管理后台,既然是给大量的用户做的,就必须要考虑到用户的权限问题,做到安全的管理,笔者以前用过的是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")
	@ResponseBody
	public String demo01(){
		//判断有没有admin的角色
		SecurityUtils.getSubject().checkRole("admin");
		System.out.println("测试代码方式验证角色");
		return "demo01";
	}
	
	
	/**
	 * 角色测试二:注解方式验证单个角色
	 * @return
	 */
	@RequiresRoles("USER")
	@RequestMapping(value="role/demo02")
	@ResponseBody
	public String demo02(){
		System.out.println("测试注解方式验证单个角色");
		return "demo02";
	}
	
	/**
	 * 角色测试三:注解方式验证多个角色
	 * @return
	 */
	@RequiresRoles(logical=Logical.OR,value={"USER","ADMIN"})
	@RequestMapping(value="role/demo03")
	@ResponseBody
	public String demo03(){
		System.out.println("测试注解方式验证多个角色");
		return "demo03";
	}
	
	
	/**
	 * 角色测试四:controller类上的注解方式验证角色(结果:不支持,只支持方法级别的***最后自定义实现了)
	 * @return
	 */
	@RequestMapping(value="role/demo04")
	@ResponseBody
	public 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());
    }
	
	
	/**
	 * 匹配带有注解的方法
	 */
	@Override
	public 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);
	}

	@Override
	public 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());
    }
	
	
	/**
	 * 匹配带有注解的方法
	 */
	@Override
	public 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{
	
	


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

}
           

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