权限管理怎么做?从“资格”与“空间”双维度设计一套可落地的权限体系

1. 引言

权限管理是几乎所有多用户系统的核心难题。很多团队在初期只做"登录 + 角色",上线后才发现:用户 A 能看用户 B 的私有数据、两个部门共用了同一份资源却互相覆盖、离职员工的权限迟迟没有回收......这些问题的根源,往往在于没有把"用户资格"和"资源空间"这两个维度分开设计。

本文从你提出的两个关键诉求出发------每个用户具有自己使用权限资源的资格 、多个用户共同占有同一个空间------梳理一套可落地的权限模型,并给出数据库设计与代码示例。

2. 先厘清两个核心概念:资格(Entitlement)与空间(Space)

在动手设计之前,先把需求拆成两个正交的维度:

  • 资格(Entitlement) :描述"某个用户能不能做某件事"。例如"张三可以编辑文档""李四可以管理成员"。它回答的是 Who can do What。
  • 空间(Space) :描述"资源归属于哪个容器"。例如"项目 A 的文档库""团队 B 的共享文件夹"。它回答的是 Where the resource lives。

你提到的"每个用户具有自己使用权限资源的资格",本质是资格要落到用户个体 ;"共同占有同样的空间",本质是多个用户共享同一个资源容器。

把这两个维度分开,就能避免最常见的错误:把"谁能访问"和"资源在哪"混在一张表里,导致共享空间里每个人的权限无法独立控制。

3. 推荐模型:RBAC + 资源空间隔离

业界最稳妥的组合是 RBAC(基于角色的访问控制)+ 资源空间(Space)隔离:

  • 用 RBAC 管理"资格":用户 → 角色 → 权限点。
  • 用 Space 管理"空间":资源挂在某个 Space 下,Space 再与用户/角色建立授权关系。

这样既保留了角色的复用性,又能做到"同一个空间里,不同用户拥有不同资格"。

3.1 核心实体

实体 说明 示例
User 用户 张三
Role 角色(一组权限的集合) 管理员、编辑者、访客
Permission 权限点(最小操作单元) doc:edit、doc:view
Space 资源空间容器 项目 A、团队 B
SpaceMember 空间成员(用户在某空间内的角色) 张三在项目 A 中是编辑者

3.2 关键设计:空间成员表

"共同占有同一个空间"的核心落点,就是 SpaceMember 这张表。它把"用户"和"空间"关联起来,并记录该用户在这个空间内的角色:

sql 复制代码
CREATE TABLE space_member (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    space_id    BIGINT NOT NULL COMMENT '空间ID',
    user_id     BIGINT NOT NULL COMMENT '用户ID',
    role_id     BIGINT NOT NULL COMMENT '角色ID(在该空间内生效)',
    created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_space_user (space_id, user_id)
) COMMENT '空间成员表:用户在某空间内的资格';

通过这张表,你可以回答两个关键问题:

  • "张三在项目 A 里能做什么?" → 查 space_member 得到角色,再映射到权限点。
  • "项目 A 里有哪些成员?" → 按 space_id 查 space_member。

4. 权限判断的核心流程

当用户发起一次操作(比如"编辑项目 A 的文档")时,判断流程如下:

  1. 找到资源所属的 Space。
  2. 查 space_member,得到该用户在这个 Space 内的 Role。
  3. 由 Role 映射出 Permission 集合。
  4. 判断所需权限点是否在集合中。
java 复制代码
public class PermissionService {

    private final SpaceMemberMapper spaceMemberMapper;
    private final RolePermissionMapper rolePermissionMapper;

    public boolean checkPermission(Long userId, Long spaceId, String permissionCode) {
        // 1. 用户在空间内的角色
        SpaceMember member = spaceMemberMapper.findBySpaceIdAndUserId(spaceId, userId);
        if (member == null) {
            return false; // 不是空间成员,默认无权限
        }

        // 2. 角色对应的权限点
        List<String> codes = rolePermissionMapper.findPermissionCodesByRoleId(member.getRoleId());
        return codes.contains(permissionCode);
    }
}

5. 如何兼顾"个人资格"与"共享空间"

你提到的两个诉求,在模型里分别对应两种授权方式:

5.1 个人专属资格

当某个用户拥有独立于任何空间的资格时(例如"全局管理员""仅查看自己的数据"),可以:

  • 在 user_role 表中记录用户的全局角色;
  • 或在 space_member 中为每个用户创建一个私有空间(Personal Space),把个人资源挂进去。
sql 复制代码
CREATE TABLE user_role (
    id      BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    role_id BIGINT NOT NULL,
    UNIQUE KEY uk_user_role (user_id, role_id)
) COMMENT '用户全局角色表:与空间无关的资格';

5.2 共享空间内的差异化资格

多个用户共同占有同一个 Space,但各自资格不同:

用户 空间 角色 可执行操作
张三 项目 A 编辑者 编辑、评论
李四 项目 A 访客 仅查看
王五 项目 A 管理员 编辑、删除、管理成员

这正是 space_member 表的价值:同一个 space_id,不同 user_id,不同 role_id。

6. 权限判断的完整时序

数据库 PermissionService 业务接口 用户 数据库 PermissionService 业务接口 用户 #mermaid-svg-G3L1k9IhwGFvzQNE{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-G3L1k9IhwGFvzQNE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-G3L1k9IhwGFvzQNE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-G3L1k9IhwGFvzQNE .error-icon{fill:#552222;}#mermaid-svg-G3L1k9IhwGFvzQNE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-G3L1k9IhwGFvzQNE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-G3L1k9IhwGFvzQNE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-G3L1k9IhwGFvzQNE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-G3L1k9IhwGFvzQNE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-G3L1k9IhwGFvzQNE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-G3L1k9IhwGFvzQNE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-G3L1k9IhwGFvzQNE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-G3L1k9IhwGFvzQNE .marker.cross{stroke:#333333;}#mermaid-svg-G3L1k9IhwGFvzQNE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-G3L1k9IhwGFvzQNE p{margin:0;}#mermaid-svg-G3L1k9IhwGFvzQNE .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-G3L1k9IhwGFvzQNE text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-G3L1k9IhwGFvzQNE .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-G3L1k9IhwGFvzQNE .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-G3L1k9IhwGFvzQNE .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-G3L1k9IhwGFvzQNE .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-G3L1k9IhwGFvzQNE #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-G3L1k9IhwGFvzQNE .sequenceNumber{fill:white;}#mermaid-svg-G3L1k9IhwGFvzQNE #sequencenumber{fill:#333;}#mermaid-svg-G3L1k9IhwGFvzQNE #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-G3L1k9IhwGFvzQNE .messageText{fill:#333;stroke:none;}#mermaid-svg-G3L1k9IhwGFvzQNE .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-G3L1k9IhwGFvzQNE .labelText,#mermaid-svg-G3L1k9IhwGFvzQNE .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-G3L1k9IhwGFvzQNE .loopText,#mermaid-svg-G3L1k9IhwGFvzQNE .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-G3L1k9IhwGFvzQNE .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-G3L1k9IhwGFvzQNE .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-G3L1k9IhwGFvzQNE .noteText,#mermaid-svg-G3L1k9IhwGFvzQNE .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-G3L1k9IhwGFvzQNE .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-G3L1k9IhwGFvzQNE .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-G3L1k9IhwGFvzQNE .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-G3L1k9IhwGFvzQNE .actorPopupMenu{position:absolute;}#mermaid-svg-G3L1k9IhwGFvzQNE .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-G3L1k9IhwGFvzQNE .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-G3L1k9IhwGFvzQNE .actor-man circle,#mermaid-svg-G3L1k9IhwGFvzQNE line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-G3L1k9IhwGFvzQNE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求编辑项目A的文档checkPermission(userId, spaceId, "doc:edit")查询 space_member返回角色 roleId查询 role_permission返回权限点集合是否包含 "doc:edit"允许 / 拒绝

7. 常见坑与建议

  • 不要把权限写死在业务代码里 :用 if (user.isAdmin()) 这种散落判断,后期会失控。统一走权限点校验。
  • 空间删除前先清理成员关系:避免产生孤儿数据。
  • 权限点要足够细:至少拆到"查看 / 编辑 / 删除 / 管理成员"级别,方便后续做细粒度控制。
  • 缓存角色-权限映射 :权限判断是高频操作,建议把 role_permission 缓存到 Redis,减少数据库压力。
  • 超管与空间管理员分开 :全局超管走 user_role,空间管理员走 space_member,两者互不干扰。

8. 总结

权限管理的核心,是把"资格"和"空间"两个维度解耦:

  • 资格用 RBAC 管理,落到用户个体;
  • 空间 用 Space 容器管理,多个用户通过 space_member 共享同一空间,并在空间内拥有各自独立的角色。

这样既满足了"每个用户具有自己使用权限资源的资格",也实现了"共同占有同样的空间"。按这个模型落地,后续无论是加新空间、调成员角色,还是做细粒度权限,都能平滑扩展。

相关推荐
starzy19901 小时前
Flink Task 调度任务深度剖析:从调度策略到故障恢复的完整链路
java·大数据·flink
zxanz11 小时前
该怎么去选择 SSL 证书来使用?有什么方法不
服务器·网络协议·ssl
无风听海1 小时前
深入理解.NET中的 Lazy<T>
服务器·php·.net
开开心心_Every1 小时前
文件查重软件批量删除重复文件释放空间
java·开发语言·c#·计算机外设·ocr·excel·perl
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 技术架构与选型剖析
java·架构·交友
我是大猴子1 小时前
一些八股和限流措施
java
小小龙学IT1 小时前
PostgreSQL 服务端机制与 SQL 实战深度解析:从 MVCC 到查询优化器
数据库·sql·postgresql
l1t1 小时前
DeepSeek总结的DuckDB 从 JSON 字符串到类型化列
数据库·sql·json
认真的小羽❅1 小时前
ThreadLocal 在虚拟线程间要小心
java·开发语言·jvm