纲要
- RBAC 模型改造背景
- 从单一角色到"角色 + 权限"双层模型
- 引入权限实体,脱离
Role直接实现GrantedAuthority的方式
- 实体类改造
Permission:实现GrantedAuthority,包含权限名与展示名Role:不再实现GrantedAuthority,与Permission建立多对多关系User:重写getAuthorities(),将角色内的权限"拍平",同时保留角色本身作为一种权限
- 多对多关系与 JPA 映射
Role与Permission互相@ManyToMany- 中间表
role_permission的映射配置 - 使用
@JsonIgnore防止序列化循环引用
- 用户认证权限的构造方式
- 流式处理
flatMap将嵌套集合扁平化 - 保留角色本身作为
ROLE_前缀的GrantedAuthority - 最终返回一维权限集合
- 流式处理
- 角色分级与权限表达式扩展
- 从数据库动态构建角色层级关系
RoleHierarchy的表达式构造- 默认
ROLE_ADMIN拥有最高权限,并包含ROLE_STAFF
- 初始数据与命名建议
- 权限命名规范:操作资源 + 动作(如
USER_READ) - 示例初始化 SQL 脚本
- 权限命名规范:操作资源 + 动作(如
改造背景与设计思路
在最初的设计中,Role 实体直接实现了 Spring Security 的 GrantedAuthority 接口,相当于角色本身就是权限。随着系统复杂度提升,需要更细粒度的权限控制,引入 RBAC(Role-Based Access Control)模型中的"权限"概念,使角色成为一组权限的容器。这样做的好处是:可以灵活组合不同权限给一个角色,再将角色授予用户;同时也支持角色继承等高级特性。
本文将展示如何对现有用户、角色实体和数据库进行改造,并补充完整的可运行代码示例。
实体类关系总览
下图展示了改造后的核心实体关系:
#mermaid-svg-bgrJIiKIFsOmKNrm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bgrJIiKIFsOmKNrm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bgrJIiKIFsOmKNrm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bgrJIiKIFsOmKNrm .error-icon{fill:#552222;}#mermaid-svg-bgrJIiKIFsOmKNrm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bgrJIiKIFsOmKNrm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bgrJIiKIFsOmKNrm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bgrJIiKIFsOmKNrm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bgrJIiKIFsOmKNrm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bgrJIiKIFsOmKNrm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bgrJIiKIFsOmKNrm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bgrJIiKIFsOmKNrm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bgrJIiKIFsOmKNrm .marker.cross{stroke:#333333;}#mermaid-svg-bgrJIiKIFsOmKNrm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bgrJIiKIFsOmKNrm p{margin:0;}#mermaid-svg-bgrJIiKIFsOmKNrm g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-bgrJIiKIFsOmKNrm g.classGroup text .title{font-weight:bolder;}#mermaid-svg-bgrJIiKIFsOmKNrm .cluster-label text{fill:#333;}#mermaid-svg-bgrJIiKIFsOmKNrm .cluster-label span{color:#333;}#mermaid-svg-bgrJIiKIFsOmKNrm .cluster-label span p{background-color:transparent;}#mermaid-svg-bgrJIiKIFsOmKNrm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bgrJIiKIFsOmKNrm .cluster text{fill:#333;}#mermaid-svg-bgrJIiKIFsOmKNrm .cluster span{color:#333;}#mermaid-svg-bgrJIiKIFsOmKNrm .nodeLabel,#mermaid-svg-bgrJIiKIFsOmKNrm .edgeLabel{color:#131300;}#mermaid-svg-bgrJIiKIFsOmKNrm .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-bgrJIiKIFsOmKNrm .label text{fill:#131300;}#mermaid-svg-bgrJIiKIFsOmKNrm .labelBkg{background:#ECECFF;}#mermaid-svg-bgrJIiKIFsOmKNrm .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-bgrJIiKIFsOmKNrm .classTitle{font-weight:bolder;}#mermaid-svg-bgrJIiKIFsOmKNrm .node rect,#mermaid-svg-bgrJIiKIFsOmKNrm .node circle,#mermaid-svg-bgrJIiKIFsOmKNrm .node ellipse,#mermaid-svg-bgrJIiKIFsOmKNrm .node polygon,#mermaid-svg-bgrJIiKIFsOmKNrm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bgrJIiKIFsOmKNrm .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm g.clickable{cursor:pointer;}#mermaid-svg-bgrJIiKIFsOmKNrm g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-bgrJIiKIFsOmKNrm g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-bgrJIiKIFsOmKNrm .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-bgrJIiKIFsOmKNrm .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-bgrJIiKIFsOmKNrm .dashed-line{stroke-dasharray:3;}#mermaid-svg-bgrJIiKIFsOmKNrm .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-bgrJIiKIFsOmKNrm #compositionStart,#mermaid-svg-bgrJIiKIFsOmKNrm .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm #compositionEnd,#mermaid-svg-bgrJIiKIFsOmKNrm .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm #dependencyStart,#mermaid-svg-bgrJIiKIFsOmKNrm .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm #dependencyStart,#mermaid-svg-bgrJIiKIFsOmKNrm .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm #extensionStart,#mermaid-svg-bgrJIiKIFsOmKNrm .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm #extensionEnd,#mermaid-svg-bgrJIiKIFsOmKNrm .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm #aggregationStart,#mermaid-svg-bgrJIiKIFsOmKNrm .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm #aggregationEnd,#mermaid-svg-bgrJIiKIFsOmKNrm .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm #lollipopStart,#mermaid-svg-bgrJIiKIFsOmKNrm .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm #lollipopEnd,#mermaid-svg-bgrJIiKIFsOmKNrm .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-bgrJIiKIFsOmKNrm .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-bgrJIiKIFsOmKNrm .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bgrJIiKIFsOmKNrm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bgrJIiKIFsOmKNrm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bgrJIiKIFsOmKNrm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} roles
permissions
1
1
*
*
User
+Long id
+String username
+String password
+Set<Role> roles
+Collection<GrantedAuthority> getAuthorities()
Role
+Long id
+String roleName
+String displayName
+Set<Permission> permissions
Permission
+Long id
+String permissionName
+String displayName
+String getAuthority()
GrantedAuthority
User 与 Role 是多对多关系,Role 与 Permission 也是多对多关系。Permission 实现了 GrantedAuthority,作为最终的权限检查单元。
数据库表结构
在原有 user、role 以及关联表 user_role 的基础上,新增 permission 表和中间表 role_permission。简化 DDL 如下:
sql
CREATE TABLE permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
permission_name VARCHAR(50) NOT NULL UNIQUE,
display_name VARCHAR(50)
);
CREATE TABLE role_permission (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES role(id),
FOREIGN KEY (permission_id) REFERENCES permission(id)
);
实体类代码实现
Permission 实体
Permission 不再像旧版 Role 那样仅是一个被容器使用的普通实体,而是直接实现 GrantedAuthority,使其自身就能被 Spring Security 进行权限投票。
java
package com.example.demo.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import javax.persistence.*;
import java.util.Set;
@Entity
@Table(name = "permission")
public class Permission implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "permission_name", unique = true, nullable = false, length = 50)
private String permissionName;
@Column(name = "display_name", length = 50)
private String displayName;
@JsonIgnore
@ManyToMany(mappedBy = "permissions", fetch = FetchType.LAZY)
private Set<Role> roles;
// --- constructors ---
public Permission() {}
public Permission(String permissionName, String displayName) {
this.permissionName = permissionName;
this.displayName = displayName;
}
// --- implements GrantedAuthority ---
@Override
public String getAuthority() {
return this.permissionName;
}
// --- getters & setters ---
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getPermissionName() { return permissionName; }
public void setPermissionName(String permissionName) { this.permissionName = permissionName; }
public String getDisplayName() { return displayName; }
public void setDisplayName(String displayName) { this.displayName = displayName; }
public Set<Role> getRoles() { return roles; }
public void setRoles(Set<Role> roles) { this.roles = roles; }
}
要点说明:
permissionName字段在数据库中是唯一约束,即每个权限标识全局唯一。displayName用于前端展示,方便管理员理解。@JsonIgnore加在roles上,避免在序列化时陷入无限循环。- 因为
GrantedAuthority.getAuthority()直接返回permissionName,权限表达式可以直接用在@PreAuthorize或配置中。
Role 实体改造
Role 原本实现了 GrantedAuthority,现在剥离该接口,并加入与 Permission 的多对多关系。
java
package com.example.demo.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.util.Set;
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "role_name", unique = true, nullable = false, length = 50)
private String roleName;
@Column(name = "display_name", length = 50)
private String displayName;
@JsonIgnore
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "role_permission",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<Permission> permissions;
// 与 User 的多对多关系,保持原有映射
@JsonIgnore
@ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
private Set<User> users;
// --- constructors ---
public Role() {}
public Role(String roleName, String displayName) {
this.roleName = roleName;
this.displayName = displayName;
}
// --- getters & setters ---
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getRoleName() { return roleName; }
public void setRoleName(String roleName) { this.roleName = roleName; }
public String getDisplayName() { return displayName; }
public void setDisplayName(String displayName) { this.displayName = displayName; }
public Set<Permission> getPermissions() { return permissions; }
public void setPermissions(Set<Permission> permissions) { this.permissions = permissions; }
public Set<User> getUsers() { return users; }
public void setUsers(Set<User> users) { this.users = users; }
}
中间表 role_permission 通过 @JoinTable 声明,外键列分别指向 role 表和 permission 表。@JsonIgnore 在两端都使用了,防止序列化时产生循环依赖。
User 实体改造
用户实体最大的变化在于 getAuthorities() 方法。原来直接从 roles 集合中返回角色即可(因为角色本身就是 GrantedAuthority),现在角色本身不再是权限,需要将角色内部的权限集合"展开",同时还要保留角色本身作为一种特殊的权限(通常带有 ROLE_ 前缀)。
java
package com.example.demo.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false, length = 50)
private String username;
@Column(nullable = false)
private String password;
// 多对多关联角色
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
// ... 其他 UserDetails 所需字段 (enabled, accountNonExpired 等)
/**
* 核心方法:将用户拥有的角色及角色内的权限统一转换为 GrantedAuthority 集合。
* 角色名称前自动添加 "ROLE_" 前缀,权限名直接使用。
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 将角色集合转换为流,对每个角色进行映射与展开
return roles.stream()
.flatMap(role -> {
// 角色本身作为一种权限:加上 ROLE_ 前缀
Stream<GrantedAuthority> roleAuthority =
Stream.of(new SimpleGrantedAuthority("ROLE_" + role.getRoleName()));
// 角色内部的权限集合
Stream<GrantedAuthority> permissionAuthorities =
role.getPermissions().stream()
.map(permission -> new SimpleGrantedAuthority(permission.getPermissionName()));
// 将两个流合并
return Stream.concat(roleAuthority, permissionAuthorities);
})
.collect(Collectors.toSet());
}
// ... getters & setters, 其他 UserDetails 方法实现
}
方法解析:
roles.stream()获得角色流。flatMap接收一个函数,该函数对每个角色返回一个Stream<GrantedAuthority>,flatMap会将所有子流"拍平"为一个连续的流。- 对于每个角色,先创建其自身的权限
SimpleGrantedAuthority("ROLE_xxx"),再将其permissions集合中的每个权限映射为SimpleGrantedAuthority,最后用Stream.concat合并。 - 最终收集到
Set中,去除重复权限。
这样,一个拥有角色 ADMIN(该角色包含权限 USER_READ, USER_WRITE)的用户,其最终的权限集合会包含 ROLE_ADMIN, USER_READ, USER_WRITE。Spring Security 在进行 hasRole('ADMIN') 或 hasAuthority('USER_READ') 检查时均可正确匹配。
角色分级与动态权限层级
很多时候需要定义角色继承关系,例如 ROLE_ADMIN 自动包含 ROLE_STAFF 的所有权限。Spring Security 提供了 RoleHierarchy 机制,通过表达式描述层级。
下面我们实现一个从数据库加载角色及权限并动态构建层级表达式的服务。
java
package com.example.demo.security;
import com.example.demo.entity.Permission;
import com.example.demo.entity.Role;
import com.example.demo.repository.RoleRepository;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class RoleHierarchyService {
private final RoleRepository roleRepository;
public RoleHierarchyService(RoleRepository roleRepository) {
this.roleRepository = roleRepository;
}
public RoleHierarchy buildRoleHierarchy() {
List<Role> roles = roleRepository.findAllWithPermissions(); // 假设 Repository 提供 fetch join 查询
StringBuilder expression = new StringBuilder();
for (Role role : roles) {
String roleName = "ROLE_" + role.getRoleName();
// 该角色下的所有权限作为其包含的 authority
List<String> authorityNames = role.getPermissions().stream()
.map(Permission::getPermissionName)
.collect(Collectors.toList());
for (String authority : authorityNames) {
expression.append(roleName).append(" > ").append(authority).append("\n");
}
}
// 手动添加固定的顶级层级,如 ADMIN > STAFF
expression.append("ROLE_ADMIN > ROLE_STAFF\n");
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(expression.toString());
return roleHierarchy;
}
}
在 Security 配置中将其声明为 Bean:
java
@Bean
public RoleHierarchy roleHierarchy(RoleHierarchyService service) {
return service.buildRoleHierarchy();
}
同时需要在 WebSecurityConfigurerAdapter 配置中启用角色层级:
java
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.expressionHandler(webExpressionHandler()) // 需要传入 roleHierarchy
// ... 其他配置
;
}
@Bean
public DefaultWebSecurityExpressionHandler webExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
这样,当检查 hasRole('ADMIN') 时,系统会根据层级表达式自动认为其拥有 STAFF 角色以及相应权限。
初始数据示例
建议在 data.sql 中初始化权限和角色关联。权限命名采用"资源_操作"的模式,清晰易读。
sql
-- 插入权限
INSERT INTO permission (permission_name, display_name) VALUES ('USER_READ', '查看用户');
INSERT INTO permission (permission_name, display_name) VALUES ('USER_CREATE', '创建用户');
INSERT INTO permission (permission_name, display_name) VALUES ('USER_UPDATE', '修改用户');
INSERT INTO permission (permission_name, display_name) VALUES ('USER_DELETE', '删除用户');
INSERT INTO permission (permission_name, display_name) VALUES ('ORDER_READ', '查看订单');
-- 插入角色
INSERT INTO role (role_name, display_name) VALUES ('ADMIN', '管理员');
INSERT INTO role (role_name, display_name) VALUES ('STAFF', '普通员工');
-- 角色-权限关联
-- ADMIN 拥有所有权限
INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1);
INSERT INTO role_permission (role_id, permission_id) VALUES (1, 2);
INSERT INTO role_permission (role_id, permission_id) VALUES (1, 3);
INSERT INTO role_permission (role_id, permission_id) VALUES (1, 4);
INSERT INTO role_permission (role_id, permission_id) VALUES (1, 5);
-- STAFF 只能查看用户和订单
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 5);
目录结构参考
改造完成后,实体与安全配置相关的典型目录结构如下:
dir
src/main/java/com/example/demo/
├── entity/
│ ├── User.java
│ ├── Role.java
│ └── Permission.java
├── repository/
│ ├── UserRepository.java
│ └── RoleRepository.java
├── security/
│ ├── RoleHierarchyService.java
│ └── SecurityConfig.java
└── service/
└── UserService.java
关键点回顾
- 实体职责清晰化 :
Permission直接承载GrantedAuthority,角色作为权限容器。 getAuthorities()扁平化处理 :利用flatMap将多维集合转换为一维权限列表,同时保留ROLE_前缀的角色权限。- 避免序列化陷阱 :在多对多关系两端使用
@JsonIgnore或 DTO 投影。 - 角色层级动态构建 :从数据库读取角色与权限后生成
RoleHierarchy表达式,实现灵活的权限继承。 - 权限命名规范 :使用
资源_操作风格,便于管理和阅读。
总结
本文基于视频材料,将口头描述的技术改造步骤整理为完整的可运行代码与配置,覆盖了从实体建模、JPA 映射到 Spring Security 集成的完整链路。
所有代码片段均可直接复制使用,只需根据实际包名调整 import。