《跟我学Shiro》笔记03-授权

原文地址:第三章 授权——《跟我学Shiro》
目录贴: 跟我学Shiro目录贴
源码:https://github.com/zhangkaitao/shiro-example

授权(访问控制)

授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。

  • 主体,即访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源。

  • 资源,在应用中用户可以访问的任何东西,比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。

  • 权限,安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:查看/新增/修改/删除用户数据(即很多时候都是 CRUD(增查改删)式权限控制)

角色

角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。

隐式角色:即直接通过角色来验证用户有没有操作权限,如在应用中 CTO、技术总监、开发工程师可以使用打印机,假设某天不允许开发工程师使用打印机,此时需要从应用中删除相应代码;再如在应用中 CTO、技术总监可以查看用户、查看权限;突然有一天不允许技术总监查看用户、查看权限了,需要在相关代码中把技术总监角色从判断逻辑中删除掉;即粒度是以角色为单位进行访问控制的,粒度较粗;如果进行修改可能造成多处代码修改。

显示角色:在程序中通过权限控制谁能访问某个资源,角色聚合一组权限集合;这样假设哪个角色不能访问某个资源,只需要从角色代表的权限集合中移除即可;无须修改多处代码;即粒度是以资源/实例为单位的;粒度较细。

授权方式

编程式

通过写 if/else 授权代码块完成:

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")) {
    //有权限
} else {
    //无权限
}

注解式

通过在执行的 Java 方法上放置相应的注解完成,没有权限将抛出相应的异常:

@RequiresRoles("admin")
public void hello() {
    //有权限
}

JSP/GSP 标签

在 JSP/GSP 页面通过相应的标签完成:

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

授权

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

ini 配置文件(shiro-role.ini)

[users]
zhang=123,role1,role2
wang=123,role1

规则即:用户名=密码,角色1,角色2,如果需要在应用中判断用户是否有相应角色,就需要在相应的 Realm 中返回角色信息,也就是说 Shiro 不负责维护用户-角色信息,需要应用提供,Shiro 只是提供相应的接口方便验证,后续会介绍如何动态的获取用户角色。

测试用例(隐式角色)

@Test(expected = UnauthorizedException.class)
public void testHasRole() {
    login("classpath:shiro-role.ini", "zhang", "123");
    // 判断拥有角色:role1
    Assert.assertTrue(subject().hasRole("role1"));
    // 判断拥有角色:role1 and role2
    Assert.assertTrue(subject().hasAllRoles(Arrays.asList("role1", "role2")));
    // 判断拥有角色:role1 and role2 and !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]);

    // 断言拥有角色:role1
    subject().checkRole("role1");
    // 断言拥有角色:role1 and role3 失败抛出异常
    subject().checkRoles("role1", "role3");
}

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

在 ini 配置文件配置用户拥有的角色及角色-权限关系(shiro-permission.ini)

[users]
zhang=123,role1,role2
wang=123,role1

[roles]
; 对资源 user 拥有 create、update 权限
role1=user:create,user:update
; 对资源 user 拥有 create、delete 权限
role2=user:create,user:delete

规则:用户名=密码,角色1,角色2角色=权限1,权限2,即首先根据用户名找到角色,然后根据角色再找到权限;即角色是权限集合;Shiro 同样不进行权限的维护,需要我们通过 Realm 返回相应的权限信息。只需要维护 用户——角色 之间的关系即可。

测试用例(显示角色)

// com.github.zhangkaitao.shiro.chapter3.PermissionTest
@Test(expected = UnauthorizedException.class)
public void testIsPermitted() {
    login("classpath: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"));

    // 断言拥有权限:user:create
    subject().checkPermission("user:create");
    // 断言拥有权限:user:delete and user:update
    subject().checkPermissions("user:delete", "user:update");
    // 断言拥有权限:user:view 失败抛出异常
    subject().checkPermissions("user:view");
}

Permission

字符串通配符权限,规则:资源标识符:操作:对象实例ID,即对哪个资源的哪个实例可以进行什么操作。其默认支持通配符权限字符串,: 表示资源/操作/实例的分割; , 表示操作的分割; * 表示任意资源/操作/实例。

单个资源权限

// 单个资源单个权限
// role3=system:user:update
subject().checkPermissions("system:user:update"); // 用户拥有资源"system:user"的"update"权限

// 单个资源多个权限
// role41=system:user:update,system:user:delete
subject().checkPermissions("system:user:update", "system:user:delete");

// 如上可以简写成:
// role42="system:user:update,delete"
subject().checkPermissions("system:user:update,delete");
// 通过 "system:user:update,delete" 验证 "system:user:update, system:user:delete" 是没问题的,但是反过来是规则不成立。

// 单个资源全部权限
// role51="system:user:create,update,delete,view"
subject().checkPermissions("system:user:create,delete,update,view");

// 如上可以简写成
// role52=system:user:* (推荐)
// role53=system:user (简写)
subject().checkPermissions("system:user:*");
subject().checkPermissions("system:user");
// 通过 "system:user:*" 验证 "system:user:create,delete,update:view" 可以,但是反过来是不成立的。

通过 system:user:update,delete 验证 system:user:update,system:user:delete 是没问题的,但是反过来是规则不成立。

// role41=system:user:update,system:user:delete
subject().checkPermissions("system:user:update", "system:user:delete");
subject().checkPermissions("system:user:update,delete"); // 报错

所有资源全部权限

// role61=*:view
subject().checkPermissions("user:view");

用户拥有所有资源的 view 所有权限。假设判断的权限是 system:user:view,那么需要 role5=*:*:view 这样写才行

实例级别的权限

// 单个实例单个权限
// role71=user:view:1  // 对资源 user 的 1 实例拥有 view 权限
subject().checkPermissions("user:view:1");

// 单个实例多个权限
// role72="user:update,delete:1" // 对资源 user 的 1 实例拥有 update、delete 权限
subject().checkPermissions("user:delete,update:1");
subject().checkPermissions("user:update:1", "user:delete:1");

// 单个实例所有权限
// role73=user:*:1 // 对资源 user 的 1 实例拥有所有权限
subject().checkPermissions("user:update:1", "user:delete:1", "user:view:1");

// 所有实例单个权限
// role74=user:auth:* // 对资源 user 的所有实例拥有 auth 权限
subject().checkPermissions("user:auth:1", "user:auth:2");

// 所有实例所有权限
// role75=user:*:* // 对资源 user 的所有实例拥有所有权限
subject().checkPermissions("user:view:1", "user:auth:2");

Shiro 对权限字符串缺失部分的处理

user:view 等价于 user:view:*;而 organization 等价于 organization:* 或者 organization:*:*。可以这么理解,这种方式实现了前缀匹配。

另外如 user:* 可以匹配如 user:deleteuser:delete 可以匹配如 user:delete:1user:*:1 可以匹配如 user:view:1user 可以匹配 user:viewuser:view:1 等。即 * 可以匹配所有,不加 * 可以进行前缀匹配;但是如 *:view 不能匹配 system:user:view ,需要使用 *:*:view ,即后缀匹配必须指定前缀(多个冒号就需要多个 * 来匹配)

WildcardPermission

// 如下两种方式是等价的
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. 如果实现了 Authorizer,那么接着调用其相应的 isPermitted*/hasRole* 接口进行匹配;
  3. 如果有一个 Realm 匹配那么将返回 true,否则返回 false

如果 Realm 进行授权的话,应该继承 AuthorizingRealm,其流程是:

  1. 如果调用 hasRole*,则直接获取 AuthorizationInfo.getRoles() 与传入的角色比较即可;
  2. 首先如果调用如 isPermitted("user:view"),首先通过 PermissionResolver 将权限字符串转换成相应的 Permission 实例,默认使用 WildcardPermissionResolver,即转换为通配符的 WildcardPermission
  3. 通过 AuthorizationInfo.getObjectPermissions() 得到 Permission 实例集合;通过 AuthorizationInfo. getStringPermissions() 得到字符串集合并通过 PermissionResolver 解析为 Permission 实例;然后获取用户的角色,并通过 RolePermissionResolver 解析角色对应的权限集合(默认没有实现,可以自己提供);
  4. 接着调用 Permission.implies(Permission p) 逐个与传入的权限比较,如果有匹配的则返回 true,否则 false

Authorizer、PermissionResolver 及 RolePermissionResolver

Authorizer 的职责是进行授权(访问控制),是 Shiro API 中授权核心的入口点,其提供了相应的角色/权限判断接口,具体请参考其 Javadoc。

SecurityManager 继承了 Authorizer 接口,且提供了 ModularRealmAuthorizer 用于多 Realm 时的授权匹配。

PermissionResolver 用于解析权限字符串到 Permission 实例。

RolePermissionResolver 用于根据角色解析相应的权限集合。

更改 Authorizer 实现

我们可以通过如下 ini 配置更改 Authorizer 实现:

对于 ModularRealmAuthorizer,相应的 AuthorizingSecurityManager 会在初始化完成后自动将相应的 realm 设置进去,我们也可以通过调用其 setRealms() 方法进行设置。对于实现自己的 authorizer 可以参考 ModularRealmAuthorizer 实现即可,在此就不提供示例了。

ini 配置更改

[main]
; 自定义 authorizer
authorizer=org.apache.shiro.authz.ModularRealmAuthorizer
; 自定义 permissionResolver
; 设置 ModularRealmAuthorizer 的 permissionResolver,其会自动设置到相应的 Realm 上(其实现了 PermissionResolverAware 接口)
; permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver
permissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.BitAndWildPermissionResolver
authorizer.permissionResolver=$permissionResolver
; 自定义 rolePermissionResolver
; 设置 ModularRealmAuthorizer 的 rolePermissionResolver,其会自动设置到相应的 Realm 上(其实现了 RolePermissionResolverAware 接口)
rolePermissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.MyRolePermissionResolver
authorizer.rolePermissionResolver=$rolePermissionResolver

securityManager.authorizer=$authorizer

; 自定义 realm 一定要放在 securityManager.authorizer 赋值之后(因为调用 setRealms 会将 realms 设置给 authorizer,
; 并给各个 Realm 设置 permissionResolver 和 rolePermissionResolver)
realm=com.github.zhangkaitao.shiro.chapter3.realm.MyRealm
securityManager.realms=$realm

设置 securityManagerrealms 一定要放到最后,因为在调用 SecurityManager.setRealms 时会将 realms 设置给 authorizer,并为各个 Realm 设置 permissionResolverrolePermissionResolver。另外,不能使用 IniSecurityManagerFactory 创建的 IniRealm,因为其初始化顺序的问题可能造成后续的初始化 Permission 造成影响。

定义 BitAndWildPermissionResolver 及 BitPermission

BitPermission 用于实现位移方式的权限,如规则是:

权限字符串格式:+资源字符串+权限位+实例ID;以 + 开头中间通过 + 分割;权限:0 表示所有权限;1 新增(二进制:0001)、2 修改(二进制:0010)、4 删除(二进制:0100)、8 查看(二进制:1000);如 +user+10 表示对资源 user 拥有修改/查看权限。

public class BitPermission implements Permission {

    private String resourceIdentify;
    private int permissionBit;
    private String instanceId;

    public BitPermission(String permissionString) {
        String[] array = permissionString.split("\\+");

        if(array.length > 1) {
            resourceIdentify = array[1];
        }
        if(StringUtils.isEmpty(resourceIdentify)) {
            resourceIdentify = "*";
        }
        if(array.length > 2) {
            permissionBit = Integer.valueOf(array[2]);
        }
        if(array.length > 3) {
            instanceId = array[3];
        }
        if(StringUtils.isEmpty(instanceId)) {
            instanceId = "*";
        }
    }

    @Override
    public boolean implies(Permission p) {
        if(!(p instanceof BitPermission)) {
            return false;
        }
        BitPermission other = (BitPermission) p;
        if(!("*".equals(this.resourceIdentify) || this.resourceIdentify.equals(other.resourceIdentify))) {
            return false;
        }
        if(!(this.permissionBit ==0 || (this.permissionBit & other.permissionBit) != 0)) {
            return false;
        }
        if(!("*".equals(this.instanceId) || this.instanceId.equals(other.instanceId))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "BitPermission{" +
                "resourceIdentify='" + resourceIdentify + '\'' +
                ", permissionBit=" + permissionBit +
                ", instanceId='" + instanceId + '\'' +
                '}';
    }
}

Permission 接口提供了 boolean implies(Permission p) 方法用于判断权限匹配的;

public class BitAndWildPermissionResolver implements PermissionResolver {

    @Override
    public Permission resolvePermission(String permissionString) {
        if(permissionString.startsWith("+")) {
            return new BitPermission(permissionString);
        }
        return new WildcardPermission(permissionString);
    }
}

BitAndWildPermissionResolver 实现了 PermissionResolver 接口,并根据权限字符串是否以 + 开头来解析权限字符串为 BitPermissionWildcardPermission

定义 MyRolePermissionResolver

RolePermissionResolver 用于根据角色字符串来解析得到权限集合。

public class MyRolePermissionResolver implements RolePermissionResolver {
    @Override
    public Collection<Permission> resolvePermissionsInRole(String roleString) {
        if("role1".equals(roleString)) {
            return Arrays.asList((Permission)new WildcardPermission("menu:*"));
        }
        return null;
    }
}

此处的实现很简单,如果用户拥有 role1,那么就返回一个 menu:* 的权限。

自定义 Realm

public class MyRealm extends AuthorizingRealm {

    // 表示根据用户身份获取授权信息
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addRole("role1");
        authorizationInfo.addRole("role2");
        authorizationInfo.addObjectPermission(new BitPermission("+user1+10"));
        authorizationInfo.addObjectPermission(new WildcardPermission("user1:*"));
        authorizationInfo.addStringPermission("+user2+10");
        authorizationInfo.addStringPermission("user2:*");
        return authorizationInfo;
    }

    // 表示获取身份验证信息
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String)token.getPrincipal();  // 得到用户名
        String password = new String((char[])token.getCredentials()); // 得到密码
        if(!"zhang".equals(username)) {
            throw new UnknownAccountException(); // 如果用户名错误
        }
        if(!"123".equals(password)) {
            throw new IncorrectCredentialsException(); // 如果密码错误
        }
        // 如果身份认证验证成功,返回一个 AuthenticationInfo 实现;
        return new SimpleAuthenticationInfo(username, password, getName());
    }
}

另外我们还可以使用 JdbcRealm,需要设置 jdbcRealm.permissionsLookupEnabledtrue 来开启权限查询。

此次还要注意就是不能把我们自定义的如 +user1+10 配置到 ini 配置文件,即使有 IniRealm 完成,因为 IniRealm 在 new 完成后就会解析这些权限字符串,默认使用了 WildcardPermissionResolver 完成,即此处是一个设计权限,如果采用生命周期(如使用初始化方法)的方式进行加载就可以解决我们自定义 permissionResolver 的问题。

测试用例

@Test
public void testIsPermitted() {
    login("classpath:shiro-authorizer.ini", "zhang", "123");
    // 判断拥有权限:user:create
    Assert.assertTrue(subject().isPermitted("user1:update"));
    Assert.assertTrue(subject().isPermitted("user1:update"));
    // 通过二进制位的方式表示权限
    Assert.assertTrue(subject().isPermitted("+user1+2")); // 新增权限
    Assert.assertTrue(subject().isPermitted("+user1+8")); // 查看权限
    Assert.assertTrue(subject().isPermitted("+user2+10")); // 新增及查看

    Assert.assertFalse(subject().isPermitted("+user1+4")); // 没有删除权限

    Assert.assertTrue(subject().isPermitted("menu:view")); // 通过MyRolePermissionResolver解析得到的权限
}

通过如上步骤可以实现自定义权限验证了。另外因为不支持 hasAnyRole/isPermittedAny 这种方式的授权,可以参考一篇《简单 shiro 扩展实现 NOT、AND、OR 权限验证 》进行简单的扩展完成这个需求,在这篇文章中通过重写 AuthorizingRealm 里的验证逻辑实现的。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《跟我学Shiro》笔记03-授权

文章字数:3.9k

本文作者:Bin

发布时间:2018-04-10, 20:12:22

最后更新:2019-08-30, 14:54:15

原始链接:http://coolview.github.io/2018/04/10/%E8%B7%9F%E6%88%91%E5%AD%A6Shiro/%E3%80%8A%E8%B7%9F%E6%88%91%E5%AD%A6Shiro%E3%80%8B%E7%AC%94%E8%AE%B003-%E6%8E%88%E6%9D%83/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录