Spring-Security安全框架的RBAC权限管理
rbac-security
这是一个基于简单的RBAC模型,结合Spring Security开发的权限管理模块。
一、RBAC模型介绍
- RBAC是Role-Based Access Control的缩写,意思就是基于角色的权限访问控制。
基本思想: 对系统的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。 同样用户被分配了多个适当的角色,那么该用户就拥有了被分配多个角色的所有权限。
优点: 不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。
该系统的RBAC模型——
二、项目思想
本项目将对用户、角色、权限三者之间的关联状态概念交给系统管理员与数据库,对外依然具有RBAC的概念。但是在项目的权限管理中,项目只维护用户与权限的关系。
- 当用户登录成功后立即在数据库查询该用户所具有的角色,再根据所拥有的角色查询对应的权限——然后将这些权限赋予用户。
- 该项目的所有接口都是基于权限级别的身份校验,而非角色级别的身份校验。
- 优点: 节省了对用户与角色、角色与权限之间关系的维护,对身份与接口之间的校验细化程度高,粒度级小。
三、项目技术架构
- 开发工具: Maven(项目构建管理)
- 开发环境: JDK8、MySql5.5.25
- 技术选型 ——
- 核心框架:SpringBoot 2.2.1.RELEASE
- 视图框架:SpringMVC 2.2.1.RELEASE
- 安全框架:SpringSecurity 2.2.1.RELEASE
- 持久层框架:Mybatis 3.5.2
- 数据库连接池:Druid 1.1.10
- 快速开发插件:Mybatis-Plus 3.2.0
- API构建工具:Swagger 2 2.9.2
四、需求说明
- 一个用户可以拥有多个角色,该用户需拥有这些角色所具有权限的并集。
- 权限的控制在于两方面——前端目录菜单的展示、后端接口的访问拦截。
- 用户登录成功后立即返回该用户可访问的菜单结构。
五、项目结构
1 | management-security |
六、重点代码解析
(注意: 这里我只展示重点代码,具体代码参考文初的GitHub地址)
1、权限数据模型
1 | /** |
- 这里的字段权限名称、权限目录名称、权限菜单名称、权限目录URL,其中权限名称是为了
Spring-Security
控制接口的拦截;权限目录名称、权限菜单名称、权限目录URL是为了给前端该用户可访问的菜单。
2、用户数据模型
1 | /** |
在这段代码中——
- 实现
org.springframework.security.core.userdetails.UserDetails
接口,将其帐号状态的一系列方法实现并对应数据库字段。 - 重点在添加字段
authorityBeans
,是权限Authority
的集合,在重写的Collection<!--? extends GrantedAuthority--> getAuthorities()
方法中,提取Authority
的字段authorityName
并将其包装为SimpleGrantedAuthority
权限对象,交给Spring-Security
维护用户权限。
3、菜单结构模型
1 | /** |
1 | /** |
- 这两个
VO
类是为了构建返回给前端标准的目录-菜单结构,目标结构效果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64[
{
"name": "孵化器管理",
"menuList": [
{
"name": "孵化器内容管理",
"url": "incubator/content"
},
{
"name": "入住企业管理",
"url": "incubator/company"
},
{
"name": "创业端轮播图管理",
"url": "incubator/rotations/entrepreneurial"
},
{
"name": "投资端轮播图管理",
"url": "incubator/rotations/investment"
},
{
"name": "会议室管理",
"url": "incubator/conference"
},
{
"name": "路演管理",
"url": "incubator/roadshow"
},
{
"name": "课程管理",
"url": "incubator/course"
}
]
},
{
"name": "订单管理",
"menuList": [
{
"name": "会员订单管理",
"url": "order/members"
},
{
"name": "保荐人订单管理",
"url": "order/sponsor"
},
{
"name": "推荐人订单管理",
"url": "order/referees"
},
{
"name": "会议室预约订单管理",
"url": "order/conference"
},
{
"name": "路演活动订单管理",
"url": "order/roadshow"
},
{
"name": "企业培训订单管理",
"url": "order/training"
}
]
}
]
4、用户源与权限的注入
1 | /** |
在这段代码中——
- 实现
org.springframework.security.core.userdetails.UserDetailsService
接口,自定义用户源。 - 重点是认证成功后,从数据库获取该用户的所有权限,自然是用户->角色->权限,
SQL
语句:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23SELECT sa.AUTHORITY_ID AS AUTHORITY_ID,
sa.AUTHORITY_NAME AS AUTHORITY_NAME,
sa.AUTHORITY_CONTENT AS AUTHORITY_CONTENT,
sa.AUTHORITY_MENU AS AUTHORITY_MENU,
sa.AUTHORITY_MENU_URL AS AUTHORITY_MENU_URL,
sa.AUTHORITY_CREAT_TIME_STAMP AS AUTHORITY_CREAT_TIME_STAMP,
sa.AUTHORITY_UPDATE_TIME_STAMP AS AUTHORITY_UPDATE_TIME_STAMP,
sa.AUTHORITY_IS_DELETE AS AUTHORITY_IS_DELETE
FROM security.security_user su,
security.security_user_role sur,
security.security_role sr,
security.security_role_authority sra,
security.security_authority sa
WHERE sa.AUTHORITY_ID = sra.AUTHORITY_ID
AND sa.AUTHORITY_IS_DELETE = 0
AND sra.ROLE_ID = sr.ROLE_ID
AND sra.ROLE_AUTHORITY_IS_DELETE = 0
AND sr.ROLE_ID = sur.ROLE_ID
AND sr.ROLE_IS_DELETE = 0
AND sur.USER_ID = su.USER_ID
AND sur.USER_ROLE_IS_DELETE = 0
AND su.USER_IS_DELETE = 0
AND su.USER_ID = #{userId}
5、对应权限菜单的返回
1 | private List<contentvo> contentBuilder(Map<string, list<contentstructure>> contentGroup) { |
在这段代码中——
- 利用
ContentStructure
类和stream
流的Collectors.groupingBy(Object object)
方法,根据Authority
类的contentName
字段,以目录名称进行分组,组建目录-菜单结构的List<contentvo>
。在这段代码中——1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30/**
* @author : zuoyu
* @description : 登陆成功行为
* @date : 2019-11-21 11:19
**/
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
private final IUserService iUserService;
public AuthenticationSuccessHandlerImpl(IUserService iUserService) {
this.iUserService = iUserService;
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
User user = (User) authentication.getPrincipal();
List<contentvo> contentList = iUserService.getContentsByUser(user);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
ServletOutputStream servletOutputStream = response.getOutputStream();
Result result = Result.detail("登陆成功", contentList);
byte[] bytes = new ObjectMapper().writeValueAsBytes(result);
servletOutputStream.write(bytes);
servletOutputStream.flush();
servletOutputStream.close();
}
} - 实现
org.springframework.security.web.authentication.AuthenticationSuccessHandler
定义登录成功后的行为; - 从已登录的身份
Authentication
中获取User
,根据User
的authorityBeans
构建菜单结构并返回。
七、关于 用户—角色 、 角色—权限 关系的修改及查询效率问题
该权限系统一共五张表,其中两张为中间表。当一个用户登录成功后,获取权限的步骤为——
- 根据用户ID去用户角色中间表获取对应的角色ID集(不走主键);
- 根据角色ID集去角色权限中间表获取这些角色ID集所对应的权限ID集(不走主键);
- 根据这些权限ID集获取权限信息。
这里有两处不走主键,所以当数据量大的时候会影响查询速度,(这里不考虑数据库索引优化),且在实际业务中一条数据是不会被真正删除的,所以当一个用户的角色修改或者一个角色的权限修改时,我们肯定不能简单粗暴的全部删除再添加。这里我利用差集的方案避免了对修改时重复数据的删除与添加。
以角色的权限修改为例——
1 |
|
- 当然对角色的删除与添加都是批处理,减少与数据库的交互。
八、本地测试
- 通过
git clone
源码 - 修改
application.yml
文件,更新MySQL账号和密码 - 第一次启动: 创建数据库
security
,数据库编码为UTF-8,并将application.yml
文件中的initialization-mode:
设置为always
,会自动建表和插入数据 - 已有数据库和表: 启动项目之前确保
application.yml
文件中的initialization-mode:
设置为never
- 在management-security目录下,执行
mvn spring-boot:run
- 本项目地址接口地址为:
http://localhost:8080/management
- 测试接口浏览器打开
http://localhost:8080/management/swagger-ui.html
- 注: idea、eclipse需安装
lombok
插件,不然会提示找不到Model
的get
、set
方法
- 已有账户与角色:
1
2
3①——账号:admin 密码:iPadAir 角色:Admin
②——账号:user 密码:123456 角色:User
③——账号:opera 密码:123456 角色:Incubator、Opera