简介

  • 授权,也叫控制访问。关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。
  • 角色:权限的集合,一般情况下我们会赋予用户角色而不是权限。
  • Shiro支持粗粒度(角色级别访问控制)和细粒度(权限级别)权限。
  • 隐式角色:直接通过角色来验证用户有没有操作权限,粒度较粗,是以角色为单位进行访问控制的。如果有一天不允许总监这一角色使用打印机了,需要在逻辑代码中将总监去掉,不允许他做的事越多,代码修改量越大。(基于角色的控制访问)
  • 显式角色:通过权限来验证用户有没有操作权限,假设哪个角色不能访问某个资源,只需要从角色的权限集合中移除某个权限即可,无需多处修改代码。(基于资源的控制访问)

授权方式

  • Shiro支持三种方式的授权:编程式、注解式和JSP/GSP标签。
  1. 编程式:写if/esle代码块;常用
Subject subject=SecurityUtils.getSubject();
if(subject.hasRole("admin")){
    //有权限
}else{
    //无权限
}
  1. 注解式:在方法上加注解(一般在controller上),没有权限将抛UnauthorizedException异常
@RequiresRoles({"admin","user"},logical=logical.AND)
public void hello() {
    //有权限
}

@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)

@RequiresAuthentication //必须通过login登录

@RequiresUser //必须已登录(身份验证或记住我)

@RequiresGuest //未登录,游客
  1. JSP/GSP标签:在JSP/GSP页面加标签

    <shiro:hasRole name="admin">
    <!— 有权限 —>
    </shiro:hasRole>
    

基于角色的访问控制(隐式角色)

  • shiro-role.ini:配置用户拥有的角色
[users]
zhang=123,role1,role2
wang=123,role1
  • BaseTest:抽象化一些常用测试方法
public abstract class BaseTest {
    /**
     * @Author haien
     * @Description protected:只能被本类及其子类、本包的方法访问
     * @Date 2019/2/18
     * @Param [configFile, username, password]
     * @return void
     **/
    protected void login(String configFile,String username,String password){
        //1、获取SecurityManager工厂
        Factory<SecurityManager> factory=new IniSecurityManagerFactory(configFile);
        //2、得到SecurityManager实例,并绑定给SecurityUtils
        SecurityManager securityManager=factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
        //3、得到Subject及创建用户名、密码身份凭证Token
        Subject subject=SecurityUtils.getSubject();
        UsernamePasswordToken token=new UsernamePasswordToken(username,password);

        subject.login(token);
    }

    public Subject subject(){
        return SecurityUtils.getSubject();
    }

    @After
    public void tearDown() throws Exception{
        ThreadContext.unbindSubject(); //退出时解除绑定Subject到线程,否则对下次测试造成影响
    }
}
  • RoleTest:测试Subject判断用户角色方法
public class RoleTest extends BaseTest{
    @Test
    public void testHasRole(){
        //zhang拥有role1、role2
        login("classpath:config/shiro-role.ini","zhang","123");
        //判断是否拥有角色:role1
        Assert.assertTrue(subject().hasRole("role1"));
        //role1、role2
        Assert.assertTrue(subject().hasAllRoles(Arrays.asList("role1","role2")));
        //role1、role2和!role3
        boolean[] result=subject().hasRoles(Arrays.asList("role1","role2","role3"));
        Assert.assertEquals(true,result[0]);
        Assert.assertEquals(true,result[1]);
        Assert.assertEquals(false,result[2]);
    }

    @Test(expected = UnauthorizedException.class)
    public void testCheckRole(){
        login("classpath:config/shiro-role.ini","zhang","123");
        //断言拥有角色role1
        subject().checkRole("role1");
        //断言拥有角色role1.role3,失败抛异常
        subject().checkRoles("role1","role3");
    }
}
  • Subject并没有提供hasAnyRole()
  • checkRole()和hasRole()的区别是它在判断为假的情况下会抛UnauthorizedException异常

    基于资源的访问控制(显式角色)

  • shiro-permission.ini
;首先根据用户名找到角色
[users]
zhang=123,role1,role2
wang=123,role1

;再根据角色找到权限
[roles]
;角色=权限1,权限2
role1=user:create,user:update
role2=user:create,user:delete
  • PermissionTest:测试细粒度的授权
public class PermissionTest extends BaseTest{
    @Test
    public void testIsPermitted(){
        login("classpath:config/shiro-permission.ini","zhang","123");
        //判断拥有权限user:create
        Assert.assertTrue(subject().isPermitted("user:create"));
        //user:update and user:delete
        Assert.assertTrue(subject().isPermittedAll("user:update","user:delete"));
        //!user:view
        Assert.assertFalse(subject().isPermitted("user:view"));
    }

    @Test(expected = UnauthorizedException.class)
    public void testCheckPermission(){
        login("classpath:config/shiro-permission.ini","zhang","123");
        //user:create
        subject().checkPermission("user:create");
        //user:create and user:update
        subject().checkPermissions("user:create","user:update");
        //!user:view,失败抛异常
        subject().checkPermissions("user:view");
    }

}
  • 没有isPermittedAny()

权限书写规范

  • 规则————资源标识符:操作:对象实例id,即对哪个资源的哪个实例可以进行什么操作。
  • 支持通配符:“:”表示资源、操作和实例之间的分隔,“,”表示操作之间的分隔,“*”表示任意资源、操作或实例。
  • 单个资源单个权限
//用户拥有system:user资源的update权限
subject().checkPermissions("system:user:update");  
  • 单个资源多个权限
role4=system:user:update,system:user:delete
//or
role4="system:user:update,delete"
//然后通过如下代码判断
subject().checkPermissions("system:user:update", "system:user:delete");
//or
subject().checkPermissions("system:user:update,delete");
  • 以上四行代码随意搭配都可
  • 单个资源全部权限
//推荐
role52=system:user:*
//or
role53=system:user
//然后通过以下代码判断
subject().checkPermissions("system:user:*");
subject().checkPermissions("system:user");
  • 所有资源某一权限
//用户拥有所有资源的view权限
role61=*:view
subject().checkPermissions("user:view");
//假设权限是system:user:view,则写法为
role5=*:*:view
  • 前缀匹配:前缀对了就能匹配任意特例
  • user等价于user:*、user:*:*
  • user:view等价于user:view:*
  • 没有后缀匹配
  • *:view不能匹配system:user:view
  • *:*:view才能匹配system:user:view
  • 性能问题:通配符陪陪方式比字符串相等匹配方式更复杂,需要花费更多时间。

    WildcardPermission

  • wildcard:通配符
  • 以下两种方式是等价的
subject().checkPermission("menu:view:1");
subject().checkPermission(new WildcardPermission("menu:view:1"));
  • 因此没什么必要的话使用字符串更方便。

流程解析

  1. 首先调用Subject.isPermitted/hasRole接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer;
  2. Authorizer是真正的授权者,如果我们调用如isPermitted(“user:view”),其首先会通过PermissionResolver把字符串转换成相应的Permission实例;
  3. 在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限;
  4. Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配则isPermitted/hasRole会返回true,否则返回false表示授权失败。
  • ModularRealmAuthorizer进行多Realm授权流程
  1. 首先检查相应的Realm是否实现了Authorizer;
  2. 是则调用相应的isPermitted/hasRole方法判断权限;
  3. 若有一个Realm匹配那么返回true,否则返回false。
  • 用Realm进行授权应继承AuthorizingRealm,授权流程:
    1.1. 如果调用hasRole*(role),则直接AuthorizationInfo.getRoles()获取用户角色与要求角色role对比即可;
    2.1.1. 如果调用isPermitted(String),首先通过AuthorizationInfo.getStringPermissions()获得字符串集合,再通过PermissionResolver将权限字符串解析为Permission实例,默认使用WildcardPermissionResolver。
    2.1.2. 如果通过isPermitted(WildcardPermission),则直接通过AuthorizationInfo.getObjectPermissions()得到Permission实例集合即可;
    2.2. 然后获取用户角色,通过RolePermissionResolver解析角色对应权限集合(无默认实现,可自己定义);
    2.3. 调用Permission.implies(Permission p)逐个与传入的用户权限比较,若有匹配的则返回true,否则返回false。

  • 总结:Authorizer的职责是授权,提供了角色、权限判断接口。其子接口为SecurityManager,它的默认实现类为ModularRealmAuthorizer用于多Realm的授权匹配。PermissionResolver用于解析要求的权限字符串到Permission实例,RolePermissionResolver用于根据角色解析相应的权限集合。

  • 指定Authorizer实现

//这里是默认实现,可以改为自定义的Authorizer实现类
authorizer=org.apache.shiro.authz.ModularRealmAuthorizer
securityManager.authorizer=$authorizer
  • PS:自定义Authorizer可参考ModularRealmAuthorizer
  • 指定Authorizer的permissionResolver(PermissionResolverAware接口实现类)
permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver
authorizer.permissionResolver=$permissionResolver
  • 指定Authorizer的rolePermissionResolver(RolePermissionResolverAware接口实现类)
rolePermissionResolver=com.github.zhangkaitao.shiro.chapter3
                            .permission.MyRolePermissionResolver
authorizer.rolePermissionResolver=$rolePermissionResolver