RBAC 实现:角色、权限、资源的建模与授权
coomia-dip 的 RBAC 层是三层权限模型的基础,负责管理"谁能做什么"的核心问题。本文详解角色继承树、权限位图、资源层级模型的设计与实现,以及如何通过 gRPC 与 Spring Security 集成实现高效的角色授权。系统支持 12 种内置角色、256 种细粒度权限、5 层资源层级,并通过 Caffeine 缓存实现亚毫秒级的权限校验。
“系列:S6 平台工程 · 第 2 篇 | 难度:高级 | 阅读时间:18 分钟
RBAC 实现:角色、权限、资源的建模与授权
#TL;DR
coomia-dip 的 RBAC 层是三层权限模型的基础,负责管理"谁能做什么"的核心问题。本文详解角色继承树、权限位图、资源层级模型的设计与实现,以及如何通过 gRPC 与 Spring Security 集成实现高效的角色授权。系统支持 12 种内置角色、256 种细粒度权限、5 层资源层级,并通过 Caffeine 缓存实现亚毫秒级的权限校验。
#1. RBAC 模型概述
#1.1 RBAC 的本质
RBAC(Role-Based Access Control)的核心思想是将权限与角色绑定,再将角色分配给用户。这种间接授权机制大大简化了权限管理:
+--------+ 分配 +--------+ 绑定 +------------+
| 用户 |----------->| 角色 |----------->| 权限 |
| User | assign | Role | bind | Permission |
+--------+ +--------+ +------------+
|
继承 | inherit
v
+--------+
| 父角色 |
| Parent |
+--------+
#1.2 coomia-dip 的 RBAC 设计目标
- 层级角色继承:子角色自动继承父角色的所有权限
- 细粒度权限控制:支持到 Object Type 和 Field 级别的权限
- 多租户隔离:权限在项目/工作区维度隔离
- 高性能评估:亚毫秒级权限校验
- 审计完备:所有角色变更和权限检查可追溯
#2. 角色模型设计
#2.1 角色继承树
coomia-dip 定义了一棵 12 个节点的角色继承树:
PLATFORM_ADMIN
/ \
SECURITY_ADMIN TENANT_ADMIN
/ / \
DATA_STEWARD PROJECT_ADMIN ONTOLOGY_ADMIN
/ \ / \ |
DATA_ENGINEER DATA_ANALYST APP_DEVELOPER ONTOLOGY_EDITOR
\ | /
\ | /
+--- VIEWER (基础只读角色) ---+
|
ANONYMOUS (未认证)
| 角色 | 级别 | 核心权限 | 典型用户 |
|---|---|---|---|
| PLATFORM_ADMIN | L0 | 全部权限 | 平台管理员 |
| SECURITY_ADMIN | L1 | 安全策略、审计、分类 | 安全官 |
| TENANT_ADMIN | L1 | 租户内全部资源 | 租户管理员 |
| DATA_STEWARD | L2 | 数据治理、血缘、质量 | 数据管家 |
| PROJECT_ADMIN | L2 | 项目内全部操作 | 项目经理 |
| ONTOLOGY_ADMIN | L2 | Ontology 管理 | 本体架构师 |
| DATA_ENGINEER | L3 | Pipeline、数据集 CRUD | 数据工程师 |
| DATA_ANALYST | L3 | 数据查询、报表、看板 | 数据分析师 |
| APP_DEVELOPER | L3 | 应用开发、Action | 应用开发者 |
| ONTOLOGY_EDITOR | L3 | ObjectType 编辑 | 本体编辑员 |
| VIEWER | L4 | 只读访问 | 普通用户 |
| ANONYMOUS | L5 | 公开资源访问 | 未认证用户 |
#2.2 角色的数据模型
message Role {
string id = 1; // 角色 ID: "role:data_analyst"
string name = 2; // 显示名称
string description = 3; // 描述
int32 level = 4; // 层级 (0=最高, 5=最低)
string parent_role_id = 5; // 父角色 ID (继承)
repeated string permission_ids = 6; // 直接绑定的权限
bool is_system = 7; // 是否系统内置角色
bool is_active = 8; // 是否启用
string tenant_id = 9; // 所属租户 (空=全局)
google.protobuf.Timestamp created_at = 10;
google.protobuf.Timestamp updated_at = 11;
}
#2.3 角色继承解析算法
public class RoleHierarchyResolver {
private final Map<String, Role> roleRegistry;
/**
* 解析角色的有效权限(含继承)
* 使用 BFS 向上遍历角色继承树
*/
public Set<Permission> resolveEffectivePermissions(String roleId) {
Set<Permission> effective = new LinkedHashSet<>();
Queue<String> queue = new LinkedList<>();
Set<String> visited = new HashSet<>();
queue.offer(roleId);
while (!queue.isEmpty()) {
String currentId = queue.poll();
if (!visited.add(currentId)) continue;
Role role = roleRegistry.get(currentId);
if (role == null) continue;
// 添加当前角色的直接权限
for (String permId : role.getPermissionIdsList()) {
effective.add(permissionRegistry.get(permId));
}
// 向上遍历父角色
if (role.hasParentRoleId()) {
queue.offer(role.getParentRoleId());
}
}
return Collections.unmodifiableSet(effective);
}
}
#3. 权限模型设计
#3.1 权限结构
每个权限由三个维度定义:操作(Action) + 资源类型(ResourceType) + 约束(Constraint)
Permission = Action x ResourceType x Constraint
示例:
READ x DATASET x {project: "alpha"}
WRITE x OBJECT_TYPE x {ontology: "customer"}
ADMIN x PROJECT x {tenant: "acme"}
#3.2 操作类型矩阵
coomia-dip 定义了 16 种标准操作,分为 4 个类别:
+-------------------+------------------------------------------+
| 类别 | 操作 |
+-------------------+------------------------------------------+
| 数据操作 (CRUD) | CREATE, READ, UPDATE, DELETE |
| 批量操作 | BATCH_READ, BATCH_WRITE, EXPORT, IMPORT |
| 管理操作 | ADMIN, CONFIGURE, GRANT, REVOKE |
| 特殊操作 | EXECUTE, SUBSCRIBE, APPROVE, SHARE |
+-------------------+------------------------------------------+
#3.3 权限位图优化
为了高效存储和比较权限,coomia-dip 使用位图(Bitmap)编码权限:
public class PermissionBitmap {
// 16 种操作 -> 16 位
private static final int CREATE = 1 << 0; // 0x0001
private static final int READ = 1 << 1; // 0x0002
private static final int UPDATE = 1 << 2; // 0x0004
private static final int DELETE = 1 << 3; // 0x0008
private static final int EXPORT = 1 << 4; // 0x0010
private static final int IMPORT = 1 << 5; // 0x0020
private static final int ADMIN = 1 << 6; // 0x0040
private static final int EXECUTE = 1 << 7; // 0x0080
private int bitmap;
public boolean hasPermission(int action) {
return (bitmap & action) == action;
}
public void grant(int action) {
bitmap |= action;
}
public void revoke(int action) {
bitmap &= ~action;
}
// 检查是否有多个权限(AND 语义)
public boolean hasAllPermissions(int... actions) {
int combined = 0;
for (int a : actions) combined |= a;
return (bitmap & combined) == combined;
}
// 检查是否有任一权限(OR 语义)
public boolean hasAnyPermission(int... actions) {
int combined = 0;
for (int a : actions) combined |= a;
return (bitmap & combined) != 0;
}
}
位图优势:
- 空间效率:一个 int (32位) 可存储所有操作权限
- 比较效率:位运算 O(1) 时间复杂度
- 合并效率:角色继承时的权限合并只需 OR 运算
#4. 资源层级模型
#4.1 五层资源层级
coomia-dip 的资源组织为 5 层层级结构:
Tenant (租户)
|
+-- Project (项目)
|
+-- Ontology (本体)
| |
| +-- ObjectType (对象类型)
| |
| +-- Property (属性/字段)
|
+-- Dataset (数据集)
| |
| +-- Column (列)
|
+-- Pipeline (管道)
|
+-- Dashboard (看板)
|
+-- Action (操作)
#4.2 资源标识符设计
资源使用分层路径标识:
格式: /{tenant}/{project}/{resource_type}/{resource_id}[/{sub_resource}]
示例:
/acme/alpha/ontology/customer # 客户本体
/acme/alpha/ontology/customer/property/phone # 电话属性
/acme/alpha/dataset/sales_2024 # 销售数据集
/acme/alpha/dashboard/kpi_board # KPI 看板
#4.3 权限继承规则
资源层级中,权限自上而下继承:
规则:
1. 租户管理员 -> 自动拥有租户内所有项目的管理权限
2. 项目管理员 -> 自动拥有项目内所有资源的管理权限
3. Ontology 管理员 -> 自动拥有 Ontology 下所有 ObjectType 的管理权限
4. ObjectType 的 READ 权限 -> 自动包含所有 Property 的 READ 权限
5. 显式设置的子资源权限可以覆盖继承的权限
继承链:
Tenant ADMIN -> Project ADMIN -> Ontology ADMIN
-> ObjectType ADMIN -> Property ADMIN
#5. gRPC 服务实现
#5.1 RoleManagementService
service RoleManagementService {
rpc CreateRole(CreateRoleRequest) returns (Role);
rpc GetRole(GetRoleRequest) returns (Role);
rpc UpdateRole(UpdateRoleRequest) returns (Role);
rpc DeleteRole(DeleteRoleRequest) returns (google.protobuf.Empty);
rpc ListRoles(ListRolesRequest) returns (ListRolesResponse);
rpc AssignRole(AssignRoleRequest) returns (RoleBinding);
rpc RevokeRole(RevokeRoleRequest) returns (google.protobuf.Empty);
rpc GetUserRoles(GetUserRolesRequest) returns (UserRolesResponse);
rpc GetEffectivePermissions(GetEffectivePermissionsRequest)
returns (EffectivePermissionsResponse);
rpc CheckPermission(CheckPermissionRequest)
returns (CheckPermissionResponse);
}
#5.2 权限检查实现
@GrpcService
public class RoleManagementServiceImpl
extends RoleManagementServiceGrpc.RoleManagementServiceImplBase {
private final RoleRepository roleRepository;
private final RoleBindingRepository bindingRepository;
private final RoleHierarchyResolver hierarchyResolver;
private final Cache<String, Set<Permission>> permissionCache;
private final AuditService auditService;
@Override
public void checkPermission(CheckPermissionRequest request,
StreamObserver<CheckPermissionResponse> observer) {
String subjectId = request.getSubjectId();
String action = request.getAction();
String resourcePath = request.getResourcePath();
// 1. 从缓存获取有效权限
Set<Permission> effective = permissionCache.get(subjectId, () -> {
List<RoleBinding> bindings = bindingRepository
.findBySubjectId(subjectId);
Set<Permission> perms = new LinkedHashSet<>();
for (RoleBinding binding : bindings) {
perms.addAll(hierarchyResolver
.resolveEffectivePermissions(binding.getRoleId()));
}
return perms;
});
// 2. 匹配权限
boolean allowed = effective.stream()
.anyMatch(p -> p.matchesAction(action)
&& p.matchesResource(resourcePath));
// 3. 审计记录
auditService.logPermissionCheck(subjectId, action,
resourcePath, allowed);
// 4. 返回结果
CheckPermissionResponse response = CheckPermissionResponse
.newBuilder()
.setAllowed(allowed)
.setReason(allowed ? "Permission granted via RBAC"
: "No matching permission found")
.build();
observer.onNext(response);
observer.onCompleted();
}
}
#6. 与 Spring Security 集成
#6.1 自定义 AuthenticationProvider
@Component
public class GrpcRbacAuthenticationProvider
implements AuthenticationProvider {
private final RoleManagementServiceBlockingStub roleService;
@Override
public Authentication authenticate(Authentication auth)
throws AuthenticationException {
String credentials = auth.getCredentials().toString();
// JWT 验证
Claims claims = jwtParser.parseClaimsJws(credentials)
.getBody();
// 从 gRPC 服务获取角色
UserRolesResponse rolesResponse = roleService.getUserRoles(
GetUserRolesRequest.newBuilder()
.setUserId(claims.getSubject())
.build()
);
List<GrantedAuthority> authorities = rolesResponse
.getRolesList()
.stream()
.map(role -> new SimpleGrantedAuthority(
"ROLE_" + role.getName()))
.collect(Collectors.toList());
return new UsernamePasswordAuthenticationToken(
claims.getSubject(), null, authorities);
}
}
#6.2 方法级权限注解
@RestController
@RequestMapping("/api/v1/ontology")
public class OntologyController {
@PreAuthorize("hasRole('ONTOLOGY_ADMIN') "
+ "or hasRole('PLATFORM_ADMIN')")
@PostMapping
public ResponseEntity<Ontology> createOntology(
@RequestBody CreateOntologyRequest request) {
return ResponseEntity.ok(ontologyService.create(request));
}
@PreAuthorize("hasRole('VIEWER')")
@GetMapping("/{id}")
public ResponseEntity<Ontology> getOntology(
@PathVariable String id) {
return ResponseEntity.ok(ontologyService.getById(id));
}
@PreAuthorize("@rbacChecker.hasResourcePermission("
+ "#id, 'UPDATE')")
@PutMapping("/{id}")
public ResponseEntity<Ontology> updateOntology(
@PathVariable String id,
@RequestBody UpdateOntologyRequest request) {
return ResponseEntity.ok(
ontologyService.update(id, request));
}
}
#7. 多租户隔离
#7.1 租户隔离架构
+-----------------------------------------------------+
| PLATFORM_ADMIN |
| (跨租户, 全局管理) |
+-----------------------------------------------------+
| | |
+--------v--------+ +-------v--------+ +------v-------+
| Tenant: ACME | | Tenant: BETA | | Tenant: GAMMA|
| | | | | |
| TENANT_ADMIN | | TENANT_ADMIN | | TENANT_ADMIN |
| PROJECT_ADMIN | | PROJECT_ADMIN | | DATA_ANALYST|
| DATA_ANALYST | | VIEWER | | VIEWER |
| VIEWER | | | | |
+-----------------+ +----------------+ +--------------+
隔离规则:
- ACME 的用户永远无法访问 BETA 的资源
- 租户间的角色完全独立
- PLATFORM_ADMIN 可以跨租户操作
#7.2 租户上下文传播
public class TenantContextInterceptor
implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String tenantId = headers.get(
Metadata.Key.of("x-tenant-id",
Metadata.ASCII_STRING_MARSHALLER));
Context ctx = Context.current()
.withValue(TENANT_ID_KEY, tenantId);
return Contexts.interceptCall(ctx, call, headers, next);
}
}
#8. 缓存策略与性能
#8.1 Caffeine 本地缓存配置
@Configuration
public class RbacCacheConfig {
@Bean
public Cache<String, Set<Permission>> permissionCache() {
return Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(30))
.recordStats()
.removalListener((key, value, cause) -> {
log.debug("Cache evicted: key={}, cause={}",
key, cause);
})
.build();
}
@Bean
public Cache<String, RoleHierarchy> hierarchyCache() {
return Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(Duration.ofMinutes(5))
.build();
}
}
#8.2 缓存失效策略
事件 -> 失效范围
-------------------------------------------------
角色权限变更 -> 该角色所有用户的权限缓存
角色继承关系变更 -> 所有层级缓存 + 受影响用户
用户角色绑定变更 -> 该用户的权限缓存
资源层级变更 -> 所有权限缓存 (全量刷新)
#8.3 性能基准
| 操作 | 缓存命中 | 缓存未命中 | P99 |
|---|---|---|---|
| 权限检查 | 0.2ms | 3.5ms | 8ms |
| 角色解析 | 0.1ms | 2.1ms | 5ms |
| 批量检查(50) | 1.5ms | 25ms | 40ms |
#9. 测试策略
#9.1 单元测试
@ExtendWith(MockitoExtension.class)
class RoleHierarchyResolverTest {
@Mock private RoleRepository roleRepository;
@InjectMocks private RoleHierarchyResolver resolver;
@Test
void shouldInheritParentPermissions() {
Role viewer = createRole("VIEWER", null,
List.of("perm:read"));
Role analyst = createRole("DATA_ANALYST", "VIEWER",
List.of("perm:export", "perm:query"));
when(roleRepository.findById("DATA_ANALYST"))
.thenReturn(Optional.of(analyst));
when(roleRepository.findById("VIEWER"))
.thenReturn(Optional.of(viewer));
Set<Permission> effective = resolver
.resolveEffectivePermissions("DATA_ANALYST");
assertThat(effective).hasSize(3);
assertThat(effective).extracting(Permission::getId)
.containsExactlyInAnyOrder(
"perm:read", "perm:export", "perm:query");
}
@Test
void shouldHandleCircularInheritance() {
Role a = createRole("A", "B", List.of("perm:a"));
Role b = createRole("B", "A", List.of("perm:b"));
when(roleRepository.findById("A"))
.thenReturn(Optional.of(a));
when(roleRepository.findById("B"))
.thenReturn(Optional.of(b));
Set<Permission> effective = resolver
.resolveEffectivePermissions("A");
assertThat(effective).hasSize(2);
}
}
#9.2 集成测试
@SpringBootTest
@Testcontainers
class RbacIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15");
@Autowired
private RoleManagementServiceBlockingStub roleService;
@Test
void fullRbacWorkflow() {
// 1. 创建自定义角色
Role customRole = roleService.createRole(
CreateRoleRequest.newBuilder()
.setName("CUSTOM_ANALYST")
.setParentRoleId("VIEWER")
.addPermissionIds("perm:query")
.addPermissionIds("perm:export")
.build()
);
// 2. 分配角色给用户
roleService.assignRole(
AssignRoleRequest.newBuilder()
.setSubjectId("user:alice")
.setRoleId(customRole.getId())
.setResourceScope("/acme/project-alpha")
.build()
);
// 3. 检查权限 - 应允许
CheckPermissionResponse response =
roleService.checkPermission(
CheckPermissionRequest.newBuilder()
.setSubjectId("user:alice")
.setAction("READ")
.setResourcePath(
"/acme/project-alpha/dataset/sales")
.build()
);
assertThat(response.getAllowed()).isTrue();
// 4. 跨项目权限 - 应拒绝
CheckPermissionResponse crossProject =
roleService.checkPermission(
CheckPermissionRequest.newBuilder()
.setSubjectId("user:alice")
.setAction("READ")
.setResourcePath(
"/acme/project-beta/dataset/hr")
.build()
);
assertThat(crossProject.getAllowed()).isFalse();
}
}
#10. 最佳实践
#10.1 角色设计原则
- 最小权限原则:用户只应拥有完成工作所需的最低权限
- 角色不超过 20 个:过多角色会导致管理混乱
- 避免直接权限分配:所有权限通过角色间接分配
- 定期审查角色:每季度审查角色分配的合理性
- 使用资源范围:权限应限定在特定项目/租户范围内
#10.2 常见反模式
反模式 1: 上帝角色
X 创建一个拥有所有权限的 "SUPER_USER" 角色
O 使用 PLATFORM_ADMIN 并限制分配人数
反模式 2: 权限蔓延
X 用户离开项目后仍保留权限
O 实现自动权限回收机制
反模式 3: 硬编码角色检查
X if (user.role == "admin") { ... }
O if (rbacChecker.hasPermission(user, "CREATE", res))
反模式 4: 共享账号
X 多人使用同一个服务账号
O 每个服务使用独立的 ServiceAccount
#Key Takeaways
- 12 个内置角色形成清晰的层级继承树,覆盖平台管理、安全、数据、开发等职责
- 权限位图使用 32 位整数编码 16 种操作,实现 O(1) 权限比较
- 5 层资源层级从租户到字段的完整资源模型,支持权限继承和覆盖
- Caffeine 缓存实现亚毫秒级权限校验,缓存命中率 85%+
- 多租户隔离通过 gRPC 上下文传播确保租户数据完全隔离
- 完整的测试覆盖包括单元测试、集成测试和循环继承等边界场景
#Next Article
下一篇 S6-03: ABAC 实现:基于属性的细粒度访问控制 将详解 ABAC 层如何通过主体属性、资源属性、环境属性的组合,实现条件化的细粒度访问控制。
Tags: #RBAC #角色管理 #权限模型 #SpringSecurity #gRPC #Caffeine #缓存 #多租户 #coomia-dip #平台工程