文章目录
- 一.RBAC 权限模型
- 1.RBAC的组成
- 2.RBAC模型分类
- 2.1.基本模型RBAC0
- 2.2.角色分层模型RBAC1
- 2.3.角色限制模型RBAC2
- 2.4.统一模型RBAC3
- 3.RBAC0模型核心表结构
- 3.1.表结构设计
- 3.2.模块划分
- 4.基于RBAC的延展—用户组
- 二.ABAC权限模型(基于Java讲解)
- 1.ABAC是什么
- 2.ABAC的条件判断控制
- 3.表达式语言(EL)
- 4.ABAC实践
- 4.1.数据库设计
- 4.2.引入依赖
- 4.3.修改配置
- 4.4.CRUD代码
- 4.5.security上下文
- 4.6.启动类
- 4.7.测试类
- 5.Spring Security 和 Apache Shiro整合
权限在日常系统中算是一个比较常见的基本功能,对于存在有权限模块的系统中规定了登录用户能够操作哪些资源,不能够操作哪些资源
。借助权限模块可以有效的控制系统不同身份人员要具体做的操作,可以说一个成熟的后端系统离不开一个比较完善的权限管理系统。
所以权限控制系统的目标就是:管理用户行为,保护系统功能。
那么如何进行权限控制呢?
- 定义资源
- 创建权限
- 创建角色
- 管理用户
- 建立关联关系
一.RBAC 权限模型
1.RBAC的组成
- 在RBAC模型里面,有3个基础组成部分,分别是:
用户、角色和权限
- User(用户):每个用户都有唯一的UID识别,并被授予不同的角色
- Role(角色):不同角色具有不同的权限
- Permission(权限):访问权限
- 用户-角色映射:用户和角色之间的映射关系
- 角色-权限映射:角色和权限之间的映射
角色权限关系
- 权限→资源:单向多对多 一个权限可以包含多个资源,一个资源可以被分配给多个不同权限
- 角色→权限:单向多对多 一个角色可以包含多个权限,一个权限可以被分配给多个不同角色
- 用户→角色:双向多对多 一个角色可以包含多个用户,一个用户可以身兼数职
如管理员和普通用户被授予不同的权限,普通用户只能去修改和查看个人信息,而不能创建用户和冻结用户,而管理员由于被授予所有权限,所以可以做所有操作。
2.RBAC模型分类
2.1.基本模型RBAC0
RBAC0是基础,很多系统只需基于RBAC0就可以搭建权限模型了。
- 在这个模型中,我们
把权限赋予角色,再把角色赋予用户
。用户和角色,角色和权限都是多对多
的关系。用户拥有的权限等于他所有的角色持有权限之和。
举个栗子:
- 譬如我们做一款企业管理产品,可以
抽象出几个角色
,譬如销售经理、财务经理、市场经理等,然后把权限分配给这些角色,再把角色赋予用户
。这样无论是分配权限还是以后的修改权限,只需要修改用户和角色的关系,或角色和权限的关系
即可,更加灵活方便。 - 此外,如果一个用户有多个角色,老王既负责销售部也负责市场部,那么可以给王先生赋予
2个角色
,即销售经理、市场经理,这样他就拥有这两个角色的所有权限
。
2.2.角色分层模型RBAC1
RBAC1建立在RBAC0基础之上,在角色中引入了 "继承" 的概念
。简单理解就是,给角色可以分成几个等级,每个等级权限不同,从而实现更细粒度的权限管理
。
举个栗子:
- 基于之前RBAC0的例子,我们又发现一个公司的销售经理可能是分几个等级的,譬如除了销售经理,还有销售副经理,而销售副经理只有销售经理的部分权限。这时候,我们就可以采用RBAC1的分级模型,把销售经理这个角色分成多个等级,给销售副经理赋予较低的等级即可。
2.3.角色限制模型RBAC2
RBAC2同样建立在RBAC0
基础之上,仅是对用户、角色和权限三者之间增加了一些限制。
- 这些限制可以分成两类,即
静态职责分离SSD
(Static Separation of Duty)和动态职责分离DSD
(Dynamic Separation of Duty)。具体限制如下图:
举个栗子:
- 还是基于之前RBAC0的例子,我们又发现有些角色之间是需要
互斥
的- 譬如给一个用户分配了销售经理的角色,就不能给他再赋予财务经理的角色了,否则他即可以
录入合同
又能自己审核合同
- 再譬如,有些公司对角色的升级十分看重,一个销售员要想升级到销售经理,必须先升级到销售主管,这时候就要采用
先决条件限制
了。
- 再譬如,有些公司对角色的升级十分看重,一个销售员要想升级到销售经理,必须先升级到销售主管,这时候就要采用
- 譬如给一个用户分配了销售经理的角色,就不能给他再赋予财务经理的角色了,否则他即可以
2.4.统一模型RBAC3
RBAC3是RBAC1和RBAC2的合集
,所以RBAC3既有**角色分层
,也包括可以增加各种限制
。**
3.RBAC0模型核心表结构
3.1.表结构设计
- 从上面实体对应关系分析,权限表设计分为以下基本的5张表结构:用户表(t_user),角色表(t_role),t_user_role(用户角色表),资源表(t_module),权限表(t_permission),表结构关系如下:
t_user 用户表
主键 id int(11) 自增 主键id
user_name varchar(20) 非空 用户名
user_pwd varchar(100) 非空 用户密码
true_name varchar(20) 可空 真实姓名
email varchar(30) 可空 邮箱
phone varchar(20) 可空 电话
is_valid int(4) 可空 有效状态
create_date datetime 可空 创建时间
update_date datetime 可空 更新时间
t_role 角色表
主键 id int(11) 自增 主键id
role_name varchar(20) 非空 角色名
role_remarker varchar(100) 可空 角色备注
is_valid int(4) 可空 有效状态
create_date datetime 可空 创建时间
t_user_role 用户角色表
主键 id int(11) 自增 主键id
user_id int(4) 非空 用户id
role_id int(4) 角色id 角色id
create_date datetime 可空 创建时间
update_date datetime 可空 更新时间
t_module 资源表
主键 id int(11) 自增 资源id
module_name varchar(20) 可空 资源名
module_style varchar(100) 可空 资源样式
url varchar(20) 可空 资源url地址
parent_id int(11) 非空 上级资源id
parent_opt_value varchar(20) 非空 上级资源权限码
grade int(11) 非空 层级
opt_value varchar(30) 可空 权限码
orders int(11) 非空 排序号
is_valid int(4) 可空 有效状态
create_date datetime 可空 创建时间
update_date datetime 可空 更新时间
t_permission 权限表
主键 id int(11) 自增 主键id
role_id int(11) 非空 角色id
module_id int(11) 非空 资源id
acl_value varchar(20) 非空 权限码
create_date datetime 可空 创建时间
update_date datetime 可空 更新时间
3.2.模块划分
从表结构设计可以看出:这里有三张主表(t_user,t_role,t_module),功能实现上这里划分为三大模块:
用户管理
- 用户基本信息维护
- 用户角色分配
角色管理
- 角色基本信息维护
- 角色授权(给角色分配能够操作的菜单)
- 角色认证(给角色拥有的权限进行校验)
资源管理
- 资源信息维护
- 菜单输出动态控制
4.基于RBAC的延展—用户组
-
基于RBAC模型,还可以适当延展,使其更适合我们的产品。譬如
增加用户组概念
,直接给用户组分配角色,再把用户加入用户
组。这样用户除了拥有自身的权限外,还拥有了所属用户组的所有权限。
举个栗子: -
譬如,我们可以把一个
部门看成一个用户组
,如销售部,财务部,再给这个部门直接赋予角色,使部门拥有部门权限,这样这个部门的所有用户都有了部门权限。
用户组概念可以更方便的给群体用户授权,且不影响用户本来就拥有的角色权限`。
二.ABAC权限模型(基于Java讲解)
1.ABAC是什么
ABAC(Attribute Base Access Control)
- 基于
属性
的权限控制不同于常见的将用户通过某种方式关联到权限的方式,ABAC则是通过动态计算一个或一组属性来是否满足某种条件来进行授权判断(可以编写简单的逻辑)。 - 属性通常来说分为四类:
用户属性(如用户年龄)
,环境属性(如当前时间)
,操作属性(如读取)和对象属性
,所以理论上能够实现非常灵活的权限控制,几乎能满足所有类型的需求。
2.ABAC的条件判断控制
基于ABAC访问控制需要动态计算实体的属性、操作类型、相关的环境来控制是否有对操作对象的权限
,所以在设计的时候需要考虑的条件判断的灵活性、通用性、易用性,用户只需要通过web页面即可配置授权,这需要减少硬编码似得逻辑变得简单通用,那么这需要满足一些运算符来实现。
类型 | 运算符 |
---|---|
算术运算符 | +, -, *, /, %, ^, div, mod |
关系运算符 | <, >, ==, !=, <=, >=, lt, gt, eq, ne, le, ge |
逻辑运算符 | and, or, not, &&, |
条件 | ?: |
- 举个栗子: 用户只需要配置 user.age > 20 的条件即可获得特定的权限。
3.表达式语言(EL)
正如上一节所说的需要对某种条件进行解析那么就需要表达式语言,使用表达式语言可以方便的访问对象中的属性、或是进行各种数学运算,条件判断等
正如:Spring Framework的@Value
注解和MyBatis的<if test=“”>
// 相信很多 Java boy都使用过的吧
@Value("A?B:C")
private String A;
<select id = "XXX">
<if test="user != null">
XXXX
</if>
</select
因此ABAC的的核心就是Expression Language(EL)
,虽然演示是Java使用作为演示,但是其他的编程语言都是有着自己的EL框架的。
java EL框架列表
-
spring-expression
-
OGNL
-
MVEL
-
JBoss EL
感兴趣可以查看 Java EL生态排名:https://mvnrepository.com/open-source/expression-languages
4.ABAC实践
4.1.数据库设计
# 用户表
DROP TABLE IF EXISTS USER;
CREATE TABLE USER (
id BIGINT(20) NOT NULL COMMENT '主键ID',
NAME VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id) COMMENT '用户表'
);
# 用户边缘数据
DROP TABLE IF EXISTS user_contribution;
CREATE TABLE user_contribution (
id BIGINT(20) NOT NULL COMMENT '主键ID',
user_id BIGINT(20) NOT NULL COMMENT '用户表ID',
repository VARCHAR(100) NOT NULL COMMENT '仓库',
PRIMARY KEY (id) COMMENT '用户边缘数据'
);
# 权限表
DROP TABLE IF EXISTS permission;
CREATE TABLE permission (
id BIGINT(20) NOT NULL COMMENT '主键ID',
permission VARCHAR(100) NOT NULL COMMENT '权限名称',
PRIMARY KEY (id) COMMENT '权限表'
);
# abac表达式表
DROP TABLE IF EXISTS abac;
CREATE TABLE abac (
id BIGINT(20) NOT NULL COMMENT '主键ID',
expression VARCHAR(100) NOT NULL COMMENT 'abac表达式',
PRIMARY KEY (id) COMMENT 'abac表达式表'
);
# abac表和权限表的关联表, o2m
DROP TABLE IF EXISTS abac_permission;
CREATE TABLE abac_permission (
id BIGINT(20) NOT NULL COMMENT '主键ID',
abac_id BIGINT(20) NOT NULL COMMENT 'abac表ID',
permission_id BIGINT(20) NOT NULL COMMENT 'permission表ID',
PRIMARY KEY (id) COMMENT 'abac表和权限表的关联表, o2m'
);
#插入用户数据
DELETE FROM USER;
INSERT INTO USER (id, NAME, age, email)
VALUES (1, '魏昌进', 26, 'mail@wcj.plus'),
(2, 'test', 1, 'mail1@wcj.plus'),
(3, 'admin', 1, 'mail2@wcj.plus');
#插入用户边缘数据
DELETE FROM user_contribution;
INSERT INTO user_contribution (id, user_id, repository)
VALUES (1, 1, 'galaxy-sea/spring-cloud-apisix'),
(2, 2, 'spring-cloud/spring-cloud-commons'),
(3, 2, 'spring-cloud/spring-cloud-openfeign'),
(4, 2, 'alibaba/spring-cloud-alibaba'),
(5, 2, 'Tencent/spring-cloud-tencent'),
(6, 2, 'apache/apisix-docker');
#插入权限数据
DELETE FROM permission;
INSERT INTO permission (id, permission)
VALUES (1, 'github:pr:merge'),
(2, 'github:pr:close'),
(3, 'github:pr:open'),
(4, 'github:pr:comment');
#插入abac表达式数据
DELETE FROM abac;
INSERT INTO abac (id, expression)
VALUES (1, 'contributions.contains(''galaxy-sea/spring-cloud-apisix'')'),
(2, 'name == ''admin'''),
(3, 'metadata.get(''ip'') == ''192.168.0.1''');
#插入abac表达式-权限映射关系数据
DELETE FROM abac_permission;
INSERT INTO abac_permission (id, abac_id, permission_id)
VALUES (1, 1, 1),
(2, 2, 1),
(3, 2, 2),
(4, 2, 3),
(5, 2, 4),
(6, 3, 1),
(7, 3, 2),
(8, 3, 3),
(9, 3, 4);
4.2.引入依赖
本章仅实现ABAC的原理,不会对Spring Security和 Apache Shiro
做任何的集成
框架
- Java 8
- Spring Boot 2.x
- MyBatis Plus 3.5.x
- MySQL 8.0
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>abac</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-abac</name>
<description>springboot-abac</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>compile</scope>
</dependency>
<!-- security权限依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.3.修改配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
password: root
username: root
driver-class-name: com.mysql.cj.jdbc.Driver
#数据库名字自己定义
url: jdbc:mysql://127.0.0.1:3306/abac_test?serverTimezone=UTC&useUnicode=true&charaterEncoding=utf-8&useSSL=false
validation-query: SELECT 1
mybatis-plus:
#配置 Maaper xml文件所在路径
mapper-locations: classpath*:/mapper/**/*.xml
#配置映射类所在的包名
type-aliases-package: com.example.entity
server:
port: 8090
4.4.CRUD代码
entiy
@Data
public class Abac {
private Long id;
private String expression;
/**
* expression对应的permissions列表
*/
@TableField(exist = false)
private List<String> permissions;
}
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
/** 用户提交过仓库 */
@TableField(exist = false)
private List<String> contributions = new ArrayList<>();
/** 存放一些乱七八糟的数据,当然contributions字段也放在这里 */
@TableField(exist = false)
private Map<String, Object> metadata = new HashMap<>();
}
dao层
@Mapper
public interface AbacDao extends BaseMapper<Abac> {
/** 获取abacId关联权限 */
@Select("SELECT p.permission\n" +
"FROM abac_permission ap LEFT JOIN permission p ON p.id = ap.permission_id\n" +
"WHERE ap.abac_id = #{abacId}")
List<String> selectPermissions(Long abacId);
}
@Mapper
public interface UserDao extends BaseMapper<User> {
/** 获取用户的仓库信息 */
@Select("SELECT repository FROM user_contribution WHERE user_id = #{userId}")
List<String> selectRepository(@Param("userId") Long userId);
}
service层
@Service
@RequiredArgsConstructor
public class AbacService {
private final AbacDao abacDao;
/** 获取abac表达式详细信息列表 */
public List<Abac> getAll() {
List<Abac> abacs = abacDao.selectList(null);
for (Abac abac : abacs) {
List<String> permissions = abacDao.selectPermissions(abac.getId());
abac.setPermissions(permissions);
}
return abacs;
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserDao userDao;
/** 根据userId获取用户详细信息 */
public User get(Long userId) {
User user = userDao.selectById(userId);
List<String> repository = userDao.selectRepository(userId);
user.setContributions(repository);
return user;
}
}
4.5.security上下文
/**
* 自定义用户元数据 用于获取一些实体的属性、操作类型、相关的环境
*/
public interface MetadataCustomizer {
/** 自定义用户元数据 */
void customize(User user);
}
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 解析abac表达式
*/
@Component
public class SecurityContext {
/**
* SpEL表达式解析器
*/
private final ExpressionParser expressionParser = new SpelExpressionParser();
/**
* 解析abac表达式
*
* @param user 用户详细信息
* @param abacs abac表达式详细信息集合
* @return expressions集合, 将这个结果集存放到 Spring Security 或者Apache APISIX的userDetail上下文中
*/
public List<String> rbacPermissions(User user, List<Abac> abacs) {
return this.rbacPermissions(user, abacs, Collections.emptyList());
}
/**
* 解析abac表达式
*
* @param user 用户详细信息
* @param abacs abac表达式详细信息集合
* @param metadataCustomizers 自定义用户元数据 用于获取一些实体的属性、操作类型、相关的环境
* @return expressions集合, 将这个结果集存放到 Spring Security 或者Apache APISIX的userDetail上下文中
*/
public List<String> rbacPermissions(User user, List<Abac> abacs, List<MetadataCustomizer> metadataCustomizers) {
// 处理自定义元数据
metadataCustomizers.forEach(metadataCustomizer -> metadataCustomizer.customize(user));
List<String> expressions = new ArrayList<>();
for (Abac abac : abacs) {
// 解析表达式的求值器
Expression expression = expressionParser.parseExpression(abac.getExpression());
// 创建环境上下文
EvaluationContext context = new StandardEvaluationContext(user);
// 获取expression的结果
if (expression.getValue(context, boolean.class)) {
expressions.addAll(abac.getPermissions());
}
}
return expressions;
}
}
4.6.启动类
@SpringBootApplication
public class SpringbootAbacApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAbacApplication.class, args);
}
}
4.7.测试类
@SpringBootTest
class AbacApplicationTests {
@Autowired
private UserService userService;
@Autowired
private AbacService abacService;
@Autowired
private SecurityContext securityContext;
/** 获取不同用户的abac权限 */
@Test
void testRbac() {
User user = userService.get(1L);
System.out.println(user);
List<Abac> rbac = abacService.getAll();
System.out.println(rbac);
List<String> permissions = securityContext.rbacPermissions(user, rbac);
System.out.println(permissions);
user = userService.get(2L);
System.out.println(user);
permissions = securityContext.rbacPermissions(user, rbac);
System.out.println(permissions);
user = userService.get(3L);
System.out.println(user);
permissions = securityContext.rbacPermissions(user, rbac);
System.out.println(permissions);
}
/**
* 获取自定义权限
*/
@Test
void testMetadataCustomizer() {
User user = userService.get(1L);
System.out.println(user);
List<Abac> rbac = abacService.getAll();
System.out.println(rbac);
List<String> permissions = securityContext.rbacPermissions(user, rbac);
System.out.println(permissions);
permissions = securityContext.rbacPermissions(user, rbac, getMetadataCustomizer());
System.out.println(permissions);
}
/** 模拟网络ip */
private List<MetadataCustomizer> getMetadataCustomizer() {
return new ArrayList<MetadataCustomizer>() {{
add(user -> user.getMetadata().put("ip", "192.168.0.1"));
}};
}
}
testRbac()测试效果
#用户1
User(id=1, name=魏昌进, age=26, email=mail@wcj.plus, contributions=[galaxy-sea/spring-cloud-apisix], metadata={})
#所有ABAC
[Abac(id=1, expression=contributions.contains('galaxy-sea/spring-cloud-apisix'), permissions=[github:pr:merge]), Abac(id=2, expression=name == 'admin', permissions=[github:pr:merge, github:pr:close, github:pr:open, github:pr:comment]), Abac(id=3, expression=metadata.get('ip') == '192.168.0.1', permissions=[github:pr:merge, github:pr:close, github:pr:open, github:pr:comment])]
#用户1权限
[github:pr:merge]
#用户2
User(id=2, name=test, age=1, email=mail1@wcj.plus, contributions=[spring-cloud/spring-cloud-commons, spring-cloud/spring-cloud-openfeign, alibaba/spring-cloud-alibaba, Tencent/spring-cloud-tencent, apache/apisix-docker], metadata={})
#用户2权限
[]
#用户3
User(id=3, name=admin, age=1, email=mail2@wcj.plus, contributions=[], metadata={})
#用户3权限
[github:pr:merge, github:pr:close, github:pr:open, github:pr:comment]
testMetadataCustomizer()测试效果
#用户1
User(id=1, name=魏昌进, age=26, email=mail@wcj.plus, contributions=[galaxy-sea/spring-cloud-apisix], metadata={})
#所有ABAC
[Abac(id=1, expression=contributions.contains('galaxy-sea/spring-cloud-apisix'), permissions=[github:pr:merge]), Abac(id=2, expression=name == 'admin', permissions=[github:pr:merge, github:pr:close, github:pr:open, github:pr:comment]), Abac(id=3, expression=metadata.get('ip') == '192.168.0.1', permissions=[github:pr:merge, github:pr:close, github:pr:open, github:pr:comment])]
#解析abac表达式,获取用户1满足条件的权限
[github:pr:merge]
#自定义用户元数据 ,判断用户1是否满足ip环境权限
[github:pr:merge, github:pr:merge, github:pr:close, github:pr:open, github:pr:comment]
5.Spring Security 和 Apache Shiro整合
Spring Security只需要修改拦截器即可在获取到UserDetails
将SecurityContext#rbacPermissions
转换为GrantedAuthority
即可
/**
* 这里是伪代码, 展示一下大概逻辑
*/
public class IamOncePerRequestFilter implements OncePerRequestFilter {
@Autowired
private SecurityContext securityContext;
@Autowired
private AbacService abacService;
@Autowired
private List<MetadataCustomizer> metadataCustomizers;
@Autowired
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
UserDetails user = toUser();
List<String> permissions = securityContext.rbacPermissions(user, abacService.getAll(), metadataCustomizers);
List<GrantedAuthority> abacAuthority = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
user.getAuthorities().addAll(abacAuthority);
}
}