Shiro 权限数据模型:用户-角色-权限树状结构设计
用户、角色、权限,这三个概念你一定不陌生。
但如何设计它们之间的关系,让权限管理既灵活又易于维护?
这一节,我们来学习经典的 RBAC 模型。
RBAC 是什么?
RBAC(Role-Based Access Control,基于角色的访问控制)是一种权限管理模型:
用户 ──拥有──▶ 角色 ──拥有──▶ 权限1
用户不直接拥有权限,而是通过角色间接拥有。
为什么需要 RBAC?
直接授权的问题
用户 ──拥有──▶ 权限1
- 用户 A 有 user:create、user:delete、order:create...
- 用户 B 有 user:view、order:view...
- 每次修改权限都要改用户
问题:权限直接绑定用户,数量爆炸,难以维护。
RBAC 的优势
用户 ──拥有──▶ 角色 ──拥有──▶ 权限1
- 创建角色「管理员」,拥有所有权限
- 创建角色「运营」,拥有部分权限
- 用户 A = 管理员角色
- 用户 B = 运营角色
优势:
- 权限按角色分组,易于管理
- 修改角色权限,所有绑定的用户同步更新
- 用户离职,只需删除用户账号
数据表设计
经典五表设计
┌─────────┐ ┌─────────────┐ ┌─────────┐
│ User │──────▶│ User_Role │◀──────│ Role │
└─────────┘ └─────────────┘ └────┬────┘
│
▼
┌───────────────┐
│ Role_Permission│◀──────┌─────────┐
└───────────────┘ │Permission│
└─────────┘1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
表结构
sql
-- 用户表
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(200) NOT NULL,
salt VARCHAR(50),
status TINYINT(1) DEFAULT 1 COMMENT '1:正常 0:禁用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 角色表
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_name VARCHAR(50) NOT NULL UNIQUE COMMENT '角色标识',
role_desc VARCHAR(100) COMMENT '角色描述',
status TINYINT(1) DEFAULT 1 COMMENT '1:正常 0:禁用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 权限表
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
permission VARCHAR(100) NOT NULL UNIQUE COMMENT '权限标识',
permission_name VARCHAR(50) NOT NULL COMMENT '权限名称',
parent_id BIGINT DEFAULT 0 COMMENT '父权限ID',
permission_type TINYINT(1) COMMENT '1:目录 2:菜单 3:按钮',
url VARCHAR(200) COMMENT '菜单URL',
icon VARCHAR(50) COMMENT '图标',
sort_order INT DEFAULT 0,
status TINYINT(1) DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 用户角色关系表
CREATE TABLE sys_user_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_role (user_id, role_id)
);
-- 角色权限关系表
CREATE TABLE sys_role_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_role_permission (role_id, permission_id)
);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
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
初始化数据
sql
-- 插入用户(密码是 SHA-256 加密后的 123456)
INSERT INTO sys_user (username, password, salt) VALUES
('admin', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'admin'),
('zhangsan', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'zhangsan'),
('lisi', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'lisi');
-- 插入角色
INSERT INTO sys_role (role_name, role_desc) VALUES
('admin', '超级管理员'),
('manager', '运营经理'),
('user', '普通用户');
-- 插入权限
INSERT INTO sys_permission (permission, permission_name, parent_id, permission_type) VALUES
('system', '系统管理', 0, 1),
('system:user', '用户管理', 1, 2),
('system:user:list', '用户列表', 2, 3),
('system:user:add', '添加用户', 2, 3),
('system:user:edit', '编辑用户', 2, 3),
('system:user:delete', '删除用户', 2, 3),
('system:role', '角色管理', 1, 2),
('system:role:list', '角色列表', 6, 3),
('system:role:add', '添加角色', 6, 3),
('order', '订单管理', 0, 1),
('order:list', '订单列表', 9, 2),
('order:view', '查看订单', 9, 3),
('order:create', '创建订单', 9, 3),
('order:cancel', '取消订单', 9, 3);
-- 分配角色权限
-- admin 拥有所有权限
INSERT INTO sys_role_permission (role_id, permission_id)
SELECT 1, id FROM sys_permission;
-- manager 拥有订单管理权限
INSERT INTO sys_role_permission (role_id, permission_id)
SELECT 2, id FROM sys_permission WHERE permission LIKE 'order:%';
-- user 只有查看权限
INSERT INTO sys_role_permission (role_id, permission_id)
SELECT 3, id FROM sys_permission
WHERE permission IN ('order:list', 'order:view');
-- 分配用户角色
INSERT INTO sys_user_role (user_id, role_id) VALUES
(1, 1), -- admin 是超级管理员
(2, 2), -- zhangsan 是运营经理
(3, 3); -- lisi 是普通用户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
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
树状结构设计
权限表支持树状结构:
sql
-- 查看权限树
SELECT
p1.id AS lv1_id, p1.permission_name AS lv1_name,
p2.id AS lv2_id, p2.permission_name AS lv2_name,
p3.id AS lv3_id, p3.permission_name AS lv3_name
FROM sys_permission p1
LEFT JOIN sys_permission p2 ON p2.parent_id = p1.id
LEFT JOIN sys_permission p3 ON p3.parent_id = p2.id
WHERE p1.parent_id = 0
ORDER BY p1.sort_order, p2.sort_order, p3.sort_order;1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
权限树结构
system(系统管理)
├── system:user(用户管理)
│ ├── system:user:list(用户列表)
│ ├── system:user:add(添加用户)
│ ├── system:user:edit(编辑用户)
│ └── system:user:delete(删除用户)
└── system:role(角色管理)
├── system:role:list(角色列表)
└── system:role:add(添加角色)
order(订单管理)
├── order:list(订单列表)
├── order:view(查看订单)
├── order:create(创建订单)
└── order:cancel(取消订单)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Mapper 实现
UserMapper
java
@Mapper
public interface UserMapper {
@Select("SELECT * FROM sys_user WHERE username = #{username}")
User findByUsername(String username);
@Select("SELECT r.* FROM sys_role r " +
"INNER JOIN sys_user_role ur ON r.id = ur.role_id " +
"WHERE ur.user_id = #{userId}")
List<Role> findRolesByUserId(Long userId);
@Select("SELECT p.* FROM sys_permission p " +
"INNER JOIN sys_role_permission rp ON p.id = rp.permission_id " +
"INNER JOIN sys_user_role ur ON rp.role_id = ur.role_id " +
"WHERE ur.user_id = #{userId}")
List<Permission> findPermissionsByUserId(Long userId);
@Select("SELECT p.permission FROM sys_permission p " +
"INNER JOIN sys_role_permission rp ON p.id = rp.permission_id " +
"INNER JOIN sys_user_role ur ON rp.role_id = ur.role_id " +
"WHERE ur.user_id = #{userId}")
List<String> findPermissionStringsByUserId(Long userId);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PermissionMapper
java
@Mapper
public interface PermissionMapper {
// 获取权限树
@Select("SELECT * FROM sys_permission ORDER BY sort_order")
List<Permission> findAll();
// 获取角色拥有的权限ID列表
@Select("SELECT permission_id FROM sys_role_permission WHERE role_id = #{roleId}")
List<Long> findPermissionIdsByRoleId(Long roleId);
// 获取角色拥有的权限树
@Select("SELECT * FROM sys_permission " +
"WHERE id IN (SELECT permission_id FROM sys_role_permission WHERE role_id = #{roleId})")
List<Permission> findPermissionsByRoleId(Long roleId);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Realm 实现
java
@Component
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
User user = userMapper.findByUsername(username);
// 查询角色
List<Role> roles = userMapper.findRolesByUserId(user.getId());
// 查询权限
List<Permission> permissions =
userMapper.findPermissionsByUserId(user.getId());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 设置角色
info.setRoles(roles.stream()
.map(Role::getRoleName)
.collect(Collectors.toSet()));
// 设置权限
info.setStringPermissions(permissions.stream()
.map(Permission::getPermission)
.collect(Collectors.toSet()));
return info;
}
// ... 认证方法省略
}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
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
权限树 API
返回前端需要的树结构
java
@Service
public class PermissionService {
@Autowired
private PermissionMapper permissionMapper;
/**
* 获取权限树(用于前端菜单渲染)
*/
public List<PermissionTree> getPermissionTree() {
List<Permission> all = permissionMapper.findAll();
// 转换为树结构
return buildTree(all, 0L);
}
private List<PermissionTree> buildTree(List<Permission> permissions, Long parentId) {
return permissions.stream()
.filter(p -> p.getParentId().equals(parentId))
.map(p -> {
PermissionTree node = new PermissionTree();
node.setId(p.getId());
node.setPermission(p.getPermission());
node.setPermissionName(p.getPermissionName());
node.setPermissionType(p.getPermissionType());
node.setUrl(p.getUrl());
node.setIcon(p.getIcon());
node.setChildren(buildTree(permissions, p.getId()));
return node;
})
.collect(Collectors.toList());
}
}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
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
角色权限分配
java
@Service
public class RoleService {
@Autowired
private RolePermissionMapper rolePermissionMapper;
/**
* 给角色分配权限
*/
public void assignPermissions(Long roleId, List<Long> permissionIds) {
// 1. 删除原有权限
rolePermissionMapper.deleteByRoleId(roleId);
// 2. 添加新权限
for (Long permissionId : permissionIds) {
RolePermission rp = new RolePermission();
rp.setRoleId(roleId);
rp.setPermissionId(permissionId);
rolePermissionMapper.insert(rp);
}
// 3. 清除该角色下所有用户的授权缓存
clearAuthorizationCache(roleId);
}
private void clearAuthorizationCache(Long roleId) {
List<Long> userIds = userRoleMapper.findUserIdsByRoleId(roleId);
for (Long userId : userIds) {
User user = userMapper.findById(userId);
// 清除缓存
authorizationCache.remove(user.getUsername());
}
}
}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
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
实际应用场景
场景一:动态菜单
java
@GetMapping("/api/menus")
@RequiresAuthentication
public Result<List<MenuVO>> getMenus() {
Subject subject = SecurityUtils.getSubject();
String username = (String) subject.getPrincipal();
// 获取用户所有权限
Set<String> permissions = subject.getPermissions();
// 过滤出菜单类型的权限
List<MenuVO> menus = permissionMapper.findMenus()
.stream()
.filter(m -> permissions.contains(m.getPermission()))
.map(this::toMenuVO)
.collect(Collectors.toList());
// 构建菜单树
return Result.success(buildMenuTree(menus));
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
场景二:数据权限
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 获取订单列表(带数据权限过滤)
*/
public List<Order> listOrders(OrderQuery query) {
Subject subject = SecurityUtils.getSubject();
// 管理员可以看所有
if (subject.hasRole("admin")) {
return orderMapper.selectList(query);
}
// 普通用户只能看自己的
String username = (String) subject.getPrincipal();
query.setUsername(username);
return orderMapper.selectList(query);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
常见问题
问题一:权限层级太深
建议:最多三层(目录 → 菜单 → 按钮)。
问题二:权限膨胀
问题:一个用户有 10 个角色,每个角色有 50 个权限,查询性能差。
方案:
- 使用缓存
- 权限按模块分组
- 定期清理无用角色和权限
问题三:跨租户权限
问题:多租户场景下,权限如何隔离?
方案:
- 权限表中增加租户 ID
- 查询时加上租户过滤
- 租户隔离由 Realm 统一处理
面试追问方向
面试官可能会问:
RBAC 模型的五个表分别是什么?
- 用户表、角色表、权限表
- 用户-角色关系表、角色-权限关系表
为什么需要中间表?
- 用户和角色是多对多关系
- 角色和权限是多对多关系
- 中间表实现多对多映射
权限表为什么要有 parent_id?
- 支持树状结构
- 方便构建菜单
- 支持批量授权
如何实现数据级别的权限?
- 在 SQL 层面加过滤条件
- 在 Realm 层面注入额外权限
- 使用自定义过滤器
留给你的问题
RBAC 解决了「能做什么」的问题,但还有「数据能否访问」的问题。
这就是数据权限。下一节,我们来学习 Shiro CSRF 防护与 URL 过滤策略。
