赞
踩
权限在日常系统中算是一个比较常见的基本功能,对于存在有权限模块的系统中规定了登录用户能够操作哪些资源,不能够操作哪些资源
。借助权限模块可以有效的控制系统不同身份人员要具体做的操作,可以说一个成熟的后端系统离不开一个比较完善的权限管理系统。
所以权限控制系统的目标就是:管理用户行为,保护系统功能。
那么如何进行权限控制呢?
用户、角色和权限
角色权限关系
- 权限→资源:单向多对多 一个权限可以包含多个资源,一个资源可以被分配给多个不同权限
- 角色→权限:单向多对多 一个角色可以包含多个权限,一个权限可以被分配给多个不同角色
- 用户→角色:双向多对多 一个角色可以包含多个用户,一个用户可以身兼数职
如管理员和普通用户被授予不同的权限,普通用户只能去修改和查看个人信息,而不能创建用户和冻结用户,而管理员由于被授予所有权限,所以可以做所有操作。
RBAC0是基础,很多系统只需基于RBAC0就可以搭建权限模型了。
把权限赋予角色,再把角色赋予用户
。用户和角色,角色和权限都是多对多
的关系。用户拥有的权限等于他所有的角色持有权限之和。举个栗子:
抽象出几个角色
,譬如销售经理、财务经理、市场经理等,然后把权限分配给这些角色,再把角色赋予用户
。这样无论是分配权限还是以后的修改权限,只需要修改用户和角色的关系,或角色和权限的关系
即可,更加灵活方便。2个角色
,即销售经理、市场经理,这样他就拥有这两个角色的所有权限
。RBAC1建立在RBAC0基础之上,在角色中引入了 "继承" 的概念
。简单理解就是,给角色可以分成几个等级,每个等级权限不同,从而实现更细粒度的权限管理
。
举个栗子:
RBAC2同样建立在RBAC0
基础之上,仅是对用户、角色和权限三者之间增加了一些限制。
静态职责分离SSD
(Static Separation of Duty)和动态职责分离DSD
(Dynamic Separation of Duty)。具体限制如下图:举个栗子:
互斥
的
录入合同
又能自己审核合同
先决条件限制
了。RBAC3是RBAC1和RBAC2的合集
,所以RBAC3既有**角色分层
,也包括可以增加各种限制
。**
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 可空 更新时间
从表结构设计可以看出:这里有三张主表(t_user,t_role,t_module),功能实现上这里划分为三大模块:
用户管理
角色管理
资源管理
基于RBAC模型,还可以适当延展,使其更适合我们的产品。譬如增加用户组概念
,直接给用户组分配角色,再把用户加入用户
组。这样用户除了拥有自身的权限外,还拥有了所属用户组的所有权限。
举个栗子:
譬如,我们可以把一个部门看成一个用户组
,如销售部,财务部,再给这个部门直接赋予角色,使部门拥有部门权限,这样这个部门的所有用户都有了部门权限。
用户组概念可以更方便的给群体用户授权,且不影响用户本来就拥有的角色权限`。
ABAC(Attribute Base Access Control)
属性
的权限控制不同于常见的将用户通过某种方式关联到权限的方式,ABAC则是通过动态计算一个或一组属性来是否满足某种条件来进行授权判断(可以编写简单的逻辑)。用户属性(如用户年龄)
, 环境属性(如当前时间)
, 操作属性(如读取)和对象属性
,所以理论上能够实现非常灵活的权限控制,几乎能满足所有类型的需求。基于ABAC访问控制需要动态计算实体的属性、操作类型、相关的环境来控制是否有对操作对象的权限
,所以在设计的时候需要考虑的条件判断的灵活性、通用性、易用性,用户只需要通过web页面即可配置授权,这需要减少硬编码似得逻辑变得简单通用,那么这需要满足一些运算符来实现。
类型 | 运算符 |
---|---|
算术运算符 | +, -, *, /, %, ^, div, mod |
关系运算符 | <, >, ==, !=, <=, >=, lt, gt, eq, ne, le, ge |
逻辑运算符 | and, or, not, &&, |
条件 | ?: |
正如上一节所说的需要对某种条件进行解析那么就需要表达式语言,使用表达式语言可以方便的访问对象中的属性、或是进行各种数学运算,条件判断等
正如: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
# 用户表
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);
本章仅实现ABAC的原理,不会对Spring Security和 Apache Shiro
做任何的集成
框架
<?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>
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
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;
}
}
/**
* 自定义用户元数据 用于获取一些实体的属性、操作类型、相关的环境
*/
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;
}
}
@SpringBootApplication
public class SpringbootAbacApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAbacApplication.class, args);
}
}
@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]
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);
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。