背景介绍:
笔者最近要开发一个开放的管理后台,既然是给大量的用户做的,就必须要考虑到用户的权限问题,做到安全的管理,笔者以前用过的是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;
}
笔者的目的:让@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扫描权限注解
#用户的请求经过被注册的注解拦截器拦截
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扫描和注解方式认证权限的过程就到这里了,当然我只是说明了部分原理,大家如果有我没有讲到的地方或者是更好的资料,希望能够分享给我,一起学习进步