学习 Shiro(三):RBAC Realm

在组织内部,通常使用 LDAP 目录服务,为组织提供统一的认证服务。

鉴权比较认证更加的复杂,系统往往需要灵活可配置的权限。

可以通过自定义 Realm 实现以下功能:

  • 认证使用 LDAP
  • 鉴权使用 JDBC

Realm

下面是 org.apache.shiro.realm.Realm 接口继承体系:

继承 org.apache.shiro.realm.AuthenticatingRealm 抽象类,即表示该 Realm 支持认证。

继承 org.apache.shiro.realm.AuthorizingRealm 抽象类,即表示该 Realm 支持认证和鉴权。

认证需要实现 doGetAuthenticationInfo(AuthenticationToken token):AuthenticationInfo 抽象方法,鉴权需要实现 doGetAuthorizationInfo(PrincipalCollection principals):AuthorizationInfo 抽象方法。

Shiro 提供了基于 LDAP 的 Realm 实现 org.apache.shiro.realm.ldap.DefaultLdapRealm 和基于 JDBC 的 Realm 实现 org.apache.shiro.realm.jdbc.JdbcRealm

自定义 Realm 可以通过继承 DefaultLdapRealm 类继承基于 LDAP 认证,通过覆盖基于 LDAP 授权实现基于 JDBC 的鉴权。

RBAC

RBAC(Role-based Access Control,基于角色的访问控制) 是一种限制已认证用户系统访问的方式。在 RBAC 中,实体(Subject)、角色(Role)和权限(Permissions)之间的 ER 图如下所示:

Subject 与 Role 是多对多的关系,Role 和 Permission 是多对多的关系。

RBAC 的特点如下:

  • 实体只能通过分配角色进行授权;
  • 可以为实体分配角色;
  • 可以为角色分配权限。

以 BI 系统为例:

小张是一名数据分析师,在 BI 系统中分配了数据分析师角色,数据分析师角色拥有编辑和查看报表的权限,小张可以编辑和查看报表。

小王是一名业务,在 BI 系统中分配了业务角色,业务角色拥有查看报表的权限,小王只可以查看报表不可以编辑报表。

LdapJdbcRealm

以 Spring Boot 集成 org.apache.shiro:shiro-spring-boot-web-starter 为例,演示如何自定义 Realm。

创建数据库表:

create table if not exists users  
(
    id          bigint auto_increment primary key,
    name        varchar(64) not null unique key
);

create table if not exists roles  
(
    id          bigint auto_increment primary key,
    name        varchar(64) not null unique key
);

create table if not exists permissions  
(
    id          bigint auto_increment primary key,
    name        varchar(64) not null unique key
);

create table if not exists user_roles  
(
    id          bigint auto_increment primary key,
    user_id     bigint not null,
    role_id     bigint not null
);

create table if not exists role_permissions  
(
    id            bigint auto_increment primary key,
    role_id       bigint not null,
    permission_id bigint not null
);

创建 RBACRealm 类:

public class RBACRealm extends DefaultLdapRealm { // ①

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }

        AuthorizationInfo info = null;

        String username = (String) getAvailablePrincipal(principals);
        if (username != null && username.trim().length() > 0) {
            try (Connection conn = this.dataSource.getConnection()) {
                Set roles = queryRolesByUsername(conn, username); // ②
                Set permissions = queryPermissionsByRoles(conn, roles); // ③
                SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(roles);
                simpleAuthorizationInfo.setStringPermissions(permissions);
                info = simpleAuthorizationInfo;
            } catch (IOException ie) {
                final String message = "There was a IO error while loading sql from classpath";
                throw new AuthorizationException(message, ie);
            } catch (SQLException se) {
                final String message = "There was a SQL error while authorizing user [" + username + "]";
                throw new AuthorizationException(message, se);
            }
        }

        return info;
    }

    private Set queryRolesByUser(Connection conn, String username) throws SQLException, IOException {
        // 省略查询
    }

    private Set queryPermissionsByUser(Connection conn, Set roleNames) throws IOException, SQLException {
        // 省略查询
    }

}

① 继承 org.apache.shiro.realm.ldap.DefaultLdapRealm 类,覆盖 doGetAuthorizationInfo(PrincipalCollection principals):AuthorizationInfo 方法;

② 通过 JDBC 查询用户角色;

③ 通过 JDBC 查询用户权限。

参考