返回博客

RBAC 实现:角色、权限、资源的建模与授权

coomia-dip 的 RBAC 层是三层权限模型的基础,负责管理"谁能做什么"的核心问题。本文详解角色继承树、权限位图、资源层级模型的设计与实现,以及如何通过 gRPC 与 Spring Security 集成实现高效的角色授权。系统支持 12 种内置角色、256 种细粒度权限、5 层资源层级,并通过 Caffeine 缓存实现亚毫秒级的权限校验。

Coomia发布于 2025年9月15日14 分钟阅读
分享本文Twitter / X

系列: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)的核心思想是将权限与角色绑定,再将角色分配给用户。这种间接授权机制大大简化了权限管理:

Code
+--------+    分配     +--------+    绑定     +------------+
| 用户    |----------->| 角色    |----------->| 权限        |
| User   |   assign   | Role   |   bind     | Permission |
+--------+            +--------+            +------------+
                          |
                     继承 | inherit
                          v
                       +--------+
                       | 父角色  |
                       | Parent |
                       +--------+

#1.2 coomia-dip 的 RBAC 设计目标

  • 层级角色继承:子角色自动继承父角色的所有权限
  • 细粒度权限控制:支持到 Object Type 和 Field 级别的权限
  • 多租户隔离:权限在项目/工作区维度隔离
  • 高性能评估:亚毫秒级权限校验
  • 审计完备:所有角色变更和权限检查可追溯

#2. 角色模型设计

#2.1 角色继承树

coomia-dip 定义了一棵 12 个节点的角色继承树:

Code
                    PLATFORM_ADMIN
                    /            \
               SECURITY_ADMIN    TENANT_ADMIN
               /                  /          \
        DATA_STEWARD     PROJECT_ADMIN   ONTOLOGY_ADMIN
        /       \           /     \            |
  DATA_ENGINEER  DATA_ANALYST  APP_DEVELOPER  ONTOLOGY_EDITOR
        \           |            /
         \          |           /
          +--- VIEWER (基础只读角色) ---+
                    |
               ANONYMOUS (未认证)
角色级别核心权限典型用户
PLATFORM_ADMINL0全部权限平台管理员
SECURITY_ADMINL1安全策略、审计、分类安全官
TENANT_ADMINL1租户内全部资源租户管理员
DATA_STEWARDL2数据治理、血缘、质量数据管家
PROJECT_ADMINL2项目内全部操作项目经理
ONTOLOGY_ADMINL2Ontology 管理本体架构师
DATA_ENGINEERL3Pipeline、数据集 CRUD数据工程师
DATA_ANALYSTL3数据查询、报表、看板数据分析师
APP_DEVELOPERL3应用开发、Action应用开发者
ONTOLOGY_EDITORL3ObjectType 编辑本体编辑员
VIEWERL4只读访问普通用户
ANONYMOUSL5公开资源访问未认证用户

#2.2 角色的数据模型

PROTOBUF
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 角色继承解析算法

Java
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)

Code
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 个类别:

Code
+-------------------+------------------------------------------+
| 类别              | 操作                                     |
+-------------------+------------------------------------------+
| 数据操作 (CRUD)    | CREATE, READ, UPDATE, DELETE              |
| 批量操作           | BATCH_READ, BATCH_WRITE, EXPORT, IMPORT  |
| 管理操作           | ADMIN, CONFIGURE, GRANT, REVOKE          |
| 特殊操作           | EXECUTE, SUBSCRIBE, APPROVE, SHARE       |
+-------------------+------------------------------------------+

#3.3 权限位图优化

为了高效存储和比较权限,coomia-dip 使用位图(Bitmap)编码权限:

Java
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 层层级结构:

Code
Tenant (租户)
  |
  +-- Project (项目)
       |
       +-- Ontology (本体)
       |    |
       |    +-- ObjectType (对象类型)
       |         |
       |         +-- Property (属性/字段)
       |
       +-- Dataset (数据集)
       |    |
       |    +-- Column (列)
       |
       +-- Pipeline (管道)
       |
       +-- Dashboard (看板)
       |
       +-- Action (操作)

#4.2 资源标识符设计

资源使用分层路径标识:

Code
格式: /{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 权限继承规则

资源层级中,权限自上而下继承:

Code
规则:
  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

PROTOBUF
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 权限检查实现

Java
@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

Java
@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 方法级权限注解

Java
@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 租户隔离架构

Code
+-----------------------------------------------------+
|                   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 租户上下文传播

Java
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 本地缓存配置

Java
@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 缓存失效策略

Code
事件                     -> 失效范围
-------------------------------------------------
角色权限变更              -> 该角色所有用户的权限缓存
角色继承关系变更          -> 所有层级缓存 + 受影响用户
用户角色绑定变更          -> 该用户的权限缓存
资源层级变更              -> 所有权限缓存 (全量刷新)

#8.3 性能基准

操作缓存命中缓存未命中P99
权限检查0.2ms3.5ms8ms
角色解析0.1ms2.1ms5ms
批量检查(50)1.5ms25ms40ms

#9. 测试策略

#9.1 单元测试

Java
@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 集成测试

Java
@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 角色设计原则

  1. 最小权限原则:用户只应拥有完成工作所需的最低权限
  2. 角色不超过 20 个:过多角色会导致管理混乱
  3. 避免直接权限分配:所有权限通过角色间接分配
  4. 定期审查角色:每季度审查角色分配的合理性
  5. 使用资源范围:权限应限定在特定项目/租户范围内

#10.2 常见反模式

Code
反模式 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

  1. 12 个内置角色形成清晰的层级继承树,覆盖平台管理、安全、数据、开发等职责
  2. 权限位图使用 32 位整数编码 16 种操作,实现 O(1) 权限比较
  3. 5 层资源层级从租户到字段的完整资源模型,支持权限继承和覆盖
  4. Caffeine 缓存实现亚毫秒级权限校验,缓存命中率 85%+
  5. 多租户隔离通过 gRPC 上下文传播确保租户数据完全隔离
  6. 完整的测试覆盖包括单元测试、集成测试和循环继承等边界场景

#Next Article

下一篇 S6-03: ABAC 实现:基于属性的细粒度访问控制 将详解 ABAC 层如何通过主体属性、资源属性、环境属性的组合,实现条件化的细粒度访问控制。

Tags: #RBAC #角色管理 #权限模型 #SpringSecurity #gRPC #Caffeine #缓存 #多租户 #coomia-dip #平台工程