目录

一、引入依赖及原理简述
二、多用户维护配置
三、基础自定义配置
四、前后端分离及登录结果管理
五、角色权限管理基础


从此处开始,为新的原创内容,相关数据结构代码换了一套新的,与之前的代码关系不大了。
建议新建一个项目,将配置文件复制过来,然后按照步骤走。

六、RBAC 结构实现 <— 你在这里 ( •̀ ω •́ )y
七、自定义响应式登录与 JWT 配置
八、集成 Redis

Spring Security(六)RBAC 结构实现

博主前言:本以为这个就是代替传统 jwt 的插件,没想到复杂程度如此之高。Spring Security 本身是个高度自定义化的组件,必须花时间重点学习一下。以下为个人配置学习的流程,从零到权限管理、redis嵌入等步骤。
本文基于尚硅谷的 Spring Security 教程学习,文章与原教程有不小出入,仅供参考。
B站视频链接:尚硅谷Java项目SpringSecurity+OAuth2权限管理实战教程

RBAC(用户 - 角色 - 权限 - 资源)是目前广泛应用的权限结构,该结构能够动态管理权限,一般为三个对象模型和两个链接模型:

RBAC_结构图

如图,用户可以有多个角色身份,角色可以被分配多种权限。

接下来我们要实现这个结构。

[!WARNING]

特殊注意,此角色非彼角色:

虽然我们设计了【角色】这个模型,但是在代码层面上,【角色】与代码的耦合度非常高——管理端和用户端的授权注解都是硬编码。

实际上,【角色】表为权限的打包集合,用于为用户分配权限及划分类别。

所以,若为单端系统,代码层面无需配置角色;若为双端乃至多端系统,代码层面每一端配置一个角色。


一、数据表实现

一共五个表,三个主表两个链接表。

  1. 用户表user
列名 数据类型 描述
id int 用户ID
username varchar 用户名
password varchar 密码
status tinyint 状态(启用/禁用)
  1. 角色表role
列名 数据类型 描述
id int 角色ID
name varchar 角色名称
type varchar 所属角色组
  1. 用户-角色链接表lk_user_role
列名 数据类型 描述
id int 用户角色关联ID
user_id int 用户ID
role_id int 角色ID
  1. 权限表permission
列名 数据类型 描述
id int 权限ID
name varchar 权限名称
value varchar 权限值
  1. 角色-权限链接表lk_role_permission
列名 数据类型 描述
id int 用户角色关联ID
role_id int 角色ID
permission_id int 权限ID

建表语句和测试数据:

[!NOTE]

构建对应的Mybatis-Plus结构这里不再赘述。

密码均为 password

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
-- 用户表
CREATE TABLE user (
id INT AUTO_INCREMENT PRIMARY KEY, -- 用户ID,自增主键
username VARCHAR(255) NOT NULL, -- 用户名,非空
password VARCHAR(255) NOT NULL, -- 密码,非空
status TINYINT(1) DEFAULT 1, -- 状态(启用/禁用),默认为1(启用)
-- 其他列可以根据需求添加
UNIQUE (username) -- 用户名唯一
);

-- 角色表
CREATE TABLE role (
id INT AUTO_INCREMENT PRIMARY KEY, -- 角色ID,自增主键
name VARCHAR(255) NOT NULL, -- 角色名称,非空
type VARCHAR(16) NOT NULL -- 所属角色组
);

-- 用户-角色链接表
CREATE TABLE lk_user_role (
id INT AUTO_INCREMENT PRIMARY KEY, -- 用户角色关联ID,自增主键
user_id INT NOT NULL, -- 用户ID,外键
role_id INT NOT NULL, -- 角色ID,外键
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE, -- 外键约束,删除用户时级联删除
FOREIGN KEY (role_id) REFERENCES role(id) ON DELETE CASCADE -- 外键约束,删除角色时级联删除
);

-- 权限表
CREATE TABLE permission (
id INT AUTO_INCREMENT PRIMARY KEY, -- 权限ID,自增主键
name VARCHAR(255) NOT NULL, -- 权限名称,非空
value VARCHAR(32) NOT NULL -- 权限描述
);

-- 角色-权限链接表
CREATE TABLE lk_role_permission (
id INT AUTO_INCREMENT PRIMARY KEY, -- 用户角色关联ID,自增主键
role_id INT NOT NULL, -- 角色ID,外键
permission_id INT NOT NULL, -- 权限ID,外键
FOREIGN KEY (role_id) REFERENCES role(id) ON DELETE CASCADE, -- 外键约束,删除角色时级联删除
FOREIGN KEY (permission_id) REFERENCES permission(id) ON DELETE CASCADE -- 外键约束,删除权限时级联删除
);

-- 测试数据
INSERT INTO `lk_role_permission` VALUES (1, 1, 1);
INSERT INTO `lk_role_permission` VALUES (2, 1, 2);
INSERT INTO `lk_role_permission` VALUES (3, 2, 2);
INSERT INTO `lk_user_role` VALUES (4, 1, 1);
INSERT INTO `lk_user_role` VALUES (5, 2, 2);
INSERT INTO `permission` VALUES (1, '查询用户列表', 'user_list');
INSERT INTO `permission` VALUES (2, '查询自己', 'user_myself');
INSERT INTO `role` VALUES (1, '管理员', 'admin');
INSERT INTO `role` VALUES (2, '普通用户', 'user');
INSERT INTO `user` VALUES (1, 'admin', '$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', 1);
INSERT INTO `user` VALUES (2, 'user', '$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', 1);


二、重写对应逻辑

重写User结构,适配UserDetails:

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
@Data
public class User implements UserDetails {
@TableId(type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private Integer status;

@TableField(exist = false)
private List<GrantedAuthority> authorities;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@Override
public String getUsername() {
return username;
}

@Override
public String getPassword() {
return password;
}

@Override
public boolean isEnabled() {
return status == 1;
}

@Override
public boolean isAccountNonExpired() {
return UserDetails.super.isAccountNonExpired();
}

@Override
public boolean isAccountNonLocked() {
return UserDetails.super.isAccountNonLocked();
}

@Override
public boolean isCredentialsNonExpired() {
return UserDetails.super.isCredentialsNonExpired();
}
}

重写UserDetailService中的用户维护校验方法:

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
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
private final PasswordEncoder passwordEncoder;
private final RoleService roleService;
private final PermissionService permissionService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 获取用户
var user = getByUsername(username);
if (user == null)
throw new UsernameNotFoundException("用户不存在:" + username);
var authorityList = new ArrayList<GrantedAuthority>();

// 获取对应角色
List<Role> roleList = roleService.getByUserId(user.getId());
roleList.forEach(role -> {
// 添加角色类型(security 中的【角色】)
authorityList.add(() -> "ROLE_" + role.getType());

// 获取对应角色权限,并添加权限
List<Permission> permissionList = permissionService.getByRoleId(role.getId());
permissionList.forEach(permission -> authorityList.add(permission::getValue));
});

// 配置权限并返回
user.setAuthorities(authorityList);
return user;
}

private User getByUsername(String username) {
return this.getOne(new LambdaQueryWrapper<User>()
.eq(User::getUsername, username));
}
}

对应的链接键查找方法:

[!NOTE]

读者可以参考这篇文章来更好的配置链接键:代码层多对多结构的通用处理

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
@Service
@RequiredArgsConstructor
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
private final LkUserRoleService lkUserRoleService;

@Override
public List<Role> getByUserId(Integer id) {
// 获取的所有链接键
var lkLQW = new LambdaQueryWrapper<LkUserRole>()
.eq(LkUserRole::getUserId, id);
var lkList = lkUserRoleService.list(lkLQW);

// 递归查找角色
var resList = new ArrayList<Role>();
lkList.forEach(lk -> resList.add(this.getById(lk.getRoleId())));
return resList;
}
}

@Service
@RequiredArgsConstructor
public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService {
private final LkRolePermissionService lkPermissionService;

@Override
public List<Permission> getByRoleId(Integer id) {
// 获取所有连接键
var lkLQW = new LambdaQueryWrapper<LkRolePermission>()
.eq(LkRolePermission::getRoleId, id);
var lkList = lkPermissionService.list(lkLQW);

// 递归查找权限
var resList = new ArrayList<Permission>();
lkList.forEach(lk -> resList.add(this.getById(lk.getPermissionId())));
return resList;
}
}

三、测试多端权限

配置Config类:

1
2
3
4
5
6
7
// 对来自 http/https 的请求的授权保护方法
http.authorizeHttpRequests(authorize -> authorize
// 配置不同终端访问权限
.requestMatchers("/admin/**").hasRole("admin")
.requestMatchers("/user/**").hasRole("user")
// 对所有请求均做授权保护,已认证的会自动授权
.anyRequest().authenticated())

编写用户列表接口:

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;

@GetMapping("/list")
public Result<List<User>> getList() {
return Result.success(userService.list());
}
}

登录并访问,发现仅user可以获取列表,admin无权限。


四、测试接口权限

配置Config类:

  • 添加@EnableMethodSecurity注解
  • 注释【配置不同终端访问权限】

修改接口:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;

@GetMapping("/list")
@PreAuthorize("hasAuthority('user_list')")
public Result<List<User>> getList() {
return Result.success(userService.list());
}
}

再次登录并访问,这一次仅admin可以获取列表,user无权限。