编辑
2024-06-05
随意记录
00
请注意,本文编写于 882 天前,最后修改于 1 天前,其中某些信息可能已经过时。

目录

统一授权与 Scope 服务设计方案
1. 目标和设计结论
1.1 与既有站点设计的关系
2. 核心概念
3. 总体架构和调用关系
3.1 业务操作与授权表责任矩阵
4. 六张核心表 ER 图
4.1 有效唯一约束
5. 角色和权限管理
6. 通用 Scope 生命周期
6.1 站点创建时序
6.2 站点删除和授权清理时序
7. 统一服务接口
7.1 GrantService.RemoveScopeBindings 内部契约
8. Scope 人员角色的差量更新
9. 鉴权和列表过滤
9.1 单个目标鉴权
9.2 列表查询
10. 实际案例
10.1 角色和权限配置
10.2 北京站点授权
10.3 同一角色绑定两个站点
10.4 站点运营人员差量编辑
10.5 通讯录和组网复用
11. 一致性、缓存和审计
11.1 一致性
11.2 缓存
11.3 审计
12. 错误语义
13. 验收场景
14. 实施前置事项

统一授权与 Scope 服务设计方案

项目内容
文档版本V1.0
日期2026-10-21
状态草稿
范围用户、角色、权限、抽象 Scope、用户角色授权、Scope 绑定
关联文档用户、角色、权限与 Scope 授权设计方案、六表参考 DDL

1. 目标和设计结论

本方案将用户、角色、权限和 Scope 授权能力收敛到统一授权插件。站点、通讯录、组网等业务插件维护业务资源及其层级;统一授权插件不复制业务 Scope 表,只保存角色、权限和资源授权绑定。业务插件不得直接写入授权表。

本方案采用以下结论:

  1. 六张授权核心表是 ums_user、ums_role、ums_permission、ums_role_permission、ums_user_role_grant、ums_grant_scope。
  2. Scope 是通用业务资源抽象,不对应授权插件中的实际表;站点、通讯录、组网等业务表是资源存在性、状态和父子层级的权威数据源。
  3. ums_grant_scope.scope_type_code 标识业务类型,scope_id 直接保存对应业务表的主键 ID;创建业务对象不需要同步创建 Scope 记录。
  4. 同一用户的同一角色在多个 Scope 生效时,只有一条有效 ums_user_role_grant,在 ums_grant_scope 中有多条有效绑定。
  5. 当前 Scope 删除完某用户的全部直接角色后,该用户从当前 Scope 的人员列表消失;其他 Scope 绑定和上级继承授权不受影响。
  6. 角色和权限必须属于同一应用;Scope 相关角色、权限和 Scope 的 scope_type_code 必须一致。
  7. 权限和 Scope 必须沿同一条 Grant 判定,禁止把一个角色的权限与另一个角色的 Scope 组合放行。
  8. 六张授权表使用 deleted_at 软删除,并使用生成列 active_unique 保证有效记录唯一、历史记录可重复保存。

1.1 与既有站点设计的关系

站点本身就是 scope_type_code=site 的 Scope,site.id 是对应的 scope_id。通讯录、组网采用相同方式映射,因此站点表不需要新增指向授权表的 scope_id 字段,也不需要同步独立的 Scope 表。站点类型、设备归属和业务生命周期仍由站点插件负责。

2. 核心概念

概念作用示例
应用隔离不同管理平台的角色和权限system、operations
权限定义能够执行什么操作site:list、site:operator:assign
角色权限集合及其范围模式站点管理员、运维人员
Scope 类型定义是哪一种业务范围site、contact、network
Scope具体业务范围实例北京站点、研发部通讯录、全国组网
Grant用户获得某个角色张三获得运维人员角色
Grant Scope某条 Grant 作用到的直接范围张三的运维人员角色绑定北京站点

scope_mode 与 scope_requirement 的规则如下:

角色 scope_mode可配置权限是否绑定 ums_grant_scope
ALL两类权限均可配置否;已配置的 Scope 相关权限覆盖全部有效同类型 Scope
SCOPE_RELATED只能配置同 Scope 类型的 SCOPE_RELATED 权限是;覆盖绑定 Scope 及其全部后代
SCOPE_INDEPENDENT只能配置 SCOPE_INDEPENDENT 权限否

ALL 表示扩大已配置权限的范围,不表示自动拥有全部权限。

3. 总体架构和调用关系

统一授权插件对外提供权限注册服务、角色服务、Grant 服务和鉴权服务。站点、通讯录、组网等业务插件按 scope_type_code 注册 ScopeResolver,供统一授权插件查询资源是否存在、是否有效以及祖先和后代范围。业务 HTTP 接口仍属于各业务插件。

flowchart LR
    UI[管理界面] --> RC[角色管理 Controller]
    UI --> SC[站点 Controller]
    UI --> CC[通讯录 Controller]
    UI --> NC[组网 Controller]

    RC --> RS[RoleManagementService]
    SC --> SS[SiteService]
    CC --> CS[ContactService]
    NC --> NS[NetworkService]

    SS --> GrantSPI[GrantService SPI]
    CS --> GrantSPI
    NS --> GrantSPI

    SS --> AuthSPI[AuthorizationService SPI]
    CS --> AuthSPI
    NS --> AuthSPI
    RS --> RoleSPI[RoleService SPI]
    RS --> PermissionSPI[PermissionService SPI]
    SS -.启动时注册站点权限.-> PermissionSPI
    CS -.启动时注册通讯录权限.-> PermissionSPI
    NS -.启动时注册组网权限.-> PermissionSPI
    SS -.注册site Resolver.-> ResolverSPI[ScopeResolverRegistry]
    CS -.注册contact Resolver.-> ResolverSPI
    NS -.注册network Resolver.-> ResolverSPI

    subgraph AP[统一授权插件]
        PermissionSPI --> AR[授权 Repository]
        RoleSPI --> AR[授权 Repository]
        GrantSPI --> AR
        AuthSPI --> AR
        GrantSPI --> ResolverSPI
        AuthSPI --> ResolverSPI
    end

    AR --> DB[(六张授权核心表)]
    SS --> SiteDB[(site及站点业务表)]
    CS --> ContactDB[(通讯录业务表)]
    NS --> NetworkDB[(组网业务表)]

调用约束:

  • 业务插件不得直接访问 ums_role、ums_user_role_grant、ums_grant_scope 等授权表。
  • 统一授权插件不得修改站点、通讯录、组网的业务正文。
  • 每个支持 Scope 的业务插件必须注册唯一的 scope_type_code 和对应 ScopeResolver;Resolver 返回的业务表数据是范围存在性、状态和层级的权威来源。
  • scope_type_code 在统一授权插件内全局唯一;即使 site.id=1 与 contact_group.id=1 同时存在,也通过不同类型形成两个独立 Scope。
  • ums_grant_scope 不对 scope_id 建数据库外键;统一授权插件在新增绑定和鉴权时必须通过对应 Resolver 校验,不能只相信调用方传参。
  • 业务插件在启动或版本迁移时通过 PermissionService 幂等注册自身权限定义;角色管理页面只把已注册权限配置给角色,不允许临时创造无法被接口使用的权限编码(这个目前可以不考虑,页面直接写死或者后端把相关页面的权限全部显示,然后操作人员进行勾选即可)。
  • 插件间优先使用进程内 SPI;HTTP 仅用于前端或跨进程部署,不作为当前单体插件之间的默认调用方式。
  • 统一授权插件负责授权表事务、并发锁、有效唯一约束处理和审计事件。

3.1 业务操作与授权表责任矩阵

业务插件拥有业务对象,统一授权插件拥有授权数据。业务插件只负责校验业务规则并调用统一授权 SPI,不得自行写入或清理授权表。

业务操作业务插件职责统一授权插件职责影响的授权表
创建业务对象校验并创建业务记录;业务主键直接作为该类型的 scope_id无持久化操作;首次授权时通过 Resolver 校验资源无
业务对象改名更新业务名称无持久化操作无
移动业务对象校验并更新业务树;通知授权缓存失效不改授权绑定;清理相关 Scope 展开缓存无
禁用或恢复业务对象更新业务状态;通知授权缓存失效不改授权绑定;后续鉴权通过 Resolver 读取最新状态无
删除业务对象校验非根节点、无子节点和无业务引用;在删除前调用 GrantService.RemoveScopeBindings撤销该 (scope_type_code, scope_id) 的直接绑定;清理已无其他有效 Scope 绑定的相关 Grant软删除 ums_grant_scope -> 按条件软删除 ums_user_role_grant
添加 Scope 人员校验业务对象存在及操作者权限;提交用户、角色和目标 Scope校验用户、角色、模式及 Scope 类型;复用或新增 Grant;新增 Grant-Scope 绑定按需新增 ums_user_role_grant、新增 ums_grant_scope
编辑人员角色提交该用户在当前 Scope 的目标角色集合,不自行计算或写授权表锁定当前直接授权,按差集新增和删除;保留其他 Scope 绑定及继承授权差量更新 ums_user_role_grant、ums_grant_scope
移除 Scope 人员提交当前 Scope、用户和空的目标角色集合撤销当前 Scope 的全部直接绑定;仅清理已无其他有效绑定的相关 Grant软删除 ums_grant_scope,按条件软删除 ums_user_role_grant
查询权限或可见范围提供登录用户、权限编码、scope_type_code 和业务主键联结角色权限和 Scope 绑定,并通过 Resolver 获取资源状态及祖先或后代范围只读六张授权表及对应业务表

其中“按条件软删除 ums_user_role_grant”只适用于 SCOPE_RELATED Grant,且必须确认该 Grant 已不存在任何有效的 ums_grant_scope。ALL 和 SCOPE_INDEPENDENT Grant 不由业务 Scope 删除操作清理。

4. 六张核心表 ER 图

todo :
1、ums_role_permission的parent_id代表什么?

erDiagram
    ums_user ||--o{ ums_user_role_grant : "获得角色"
    ums_role ||--o{ ums_user_role_grant : "被授予"
    ums_role ||--o{ ums_role_permission : "配置权限"
    ums_permission ||--o{ ums_role_permission : "被角色引用"
    ums_user_role_grant ||--o{ ums_grant_scope : "绑定直接范围"

    ums_user {
        bigint id PK
        varchar account UK
        varchar identity_source
        tinyint status
        datetime deleted_at
        tinyint active_unique
    }
    ums_role {
        bigint id PK
        varchar application_code
        varchar code
        varchar scope_mode
        varchar scope_type_code
        tinyint status
        datetime deleted_at
        tinyint active_unique
    }
    ums_permission {
        bigint id PK
        bigint parent_id FK
        varchar application_code
        varchar code
        varchar permission_type
        varchar scope_requirement
        varchar scope_type_code
        datetime deleted_at
        tinyint active_unique
    }
    ums_role_permission {
        bigint id PK
        bigint role_id FK
        bigint permission_id FK
        tinyint status
        datetime deleted_at
        tinyint active_unique
    }
    ums_user_role_grant {
        bigint id PK
        bigint user_id FK
        bigint role_id FK
        tinyint status
        datetime deleted_at
        tinyint active_unique
    }
    ums_grant_scope {
        bigint id PK
        bigint grant_id FK
        varchar scope_type_code
        bigint scope_id
        tinyint status
        datetime deleted_at
        tinyint active_unique
    }

ums_grant_scope.scope_id 是多态业务引用:当 scope_type_code=site 时引用 site.id,为 contact 时引用通讯录业务表主键,为 network 时引用组网业务表主键。由于一列可能引用不同业务表,数据库不能建立统一外键,资源有效性由对应的 ScopeResolver 保证。

完整字段、索引、外键和检查约束见具体表设计 --参考 DDL。

4.1 有效唯一约束

所有表使用以下生成列:

sql
active_unique TINYINT GENERATED ALWAYS AS ( CASE WHEN deleted_at IS NULL THEN 1 ELSE NULL END ) VIRTUAL

有效记录的 active_unique 为 1,参与唯一索引并阻止重复;软删除记录的值为 NULL。MySQL 唯一索引允许多条 NULL,因此可以保留多次授权和撤销历史。

关键有效唯一关系为:

text
用户账号 (account, active_unique) 应用内角色编码 (application_code, code, active_unique) 应用内权限编码 (application_code, code, active_unique) 角色权限 (role_id, permission_id, active_unique) 用户角色Grant (user_id, role_id, active_unique) Grant直接Scope绑定 (grant_id, scope_type_code, scope_id, active_unique)

5. 角色和权限管理

角色管理页面按应用、范围模式和 Scope 类型构造分组,不把页面分组保存为角色父子关系:

text
系统管理角色 运营平台角色 ├─ 平台角色 ├─ 站点相关角色 └─ 站点无关角色

对应规则:

创建位置application_codescope_modescope_type_code
系统管理角色system通常为 SCOPE_INDEPENDENTNULL
运营平台根分组operationsALLNULL
站点相关角色operationsSCOPE_RELATEDsite
站点无关角色operationsSCOPE_INDEPENDENTNULL

保存角色权限时必须校验:

  1. 角色和权限有效且属于同一 application_code。
  2. SCOPE_RELATED 角色只能配置相同 scope_type_code 的相关权限。
  3. SCOPE_INDEPENDENT 角色只能配置无关权限。
  4. ALL 可配置两类权限,但只能使用明确写入 ums_role_permission 的权限。

todo: 权限编码由实际提供接口的插件负责声明,例如站点插件注册 site:list、site:update 和 site:operator:assign。注册操作使用 (application_code, code) 幂等更新名称、展示树和范围要求;改变既有权限的 scope_requirement 或 scope_type_code 属于不兼容变更,必须先完成角色配置迁移,不能在运行时静默修改。

6. 通用 Scope 生命周期

Scope 由 (scope_type_code, scope_id) 唯一描述,其中 scope_id 直接使用业务表主键:

text
(site, site.id) (contact, contact_group.id) (network, network.id)

Scope 的名称、父子关系、状态和软删除状态均来自对应业务表。统一授权插件不保存这些字段,只通过注册的 ScopeResolver 查询。因此:

业务动作授权协作授权结果
创建业务对象无需创建 Scope 记录新对象自动成为可引用 Scope;上级授权可按业务树覆盖它
改名无需调用授权服务不影响授权
移动通知授权服务失效该类型的范围展开缓存绑定记录不变,继承范围按新业务树重新计算
禁用或恢复通知授权服务失效相关缓存鉴权通过 Resolver 读取最新状态
删除删除前调用 GrantService.RemoveScopeBindings清理该业务资源的直接绑定,不影响其他业务资源

父子关系合法性、移动成环检查和业务状态规则由业务插件负责。ScopeResolver 必须在授权判断中返回包含目标自身的祖先链;找不到目标、目标无效或祖先无效时应失败关闭。

6.1 站点创建时序

站点创建不再写入授权表,也不需要回填授权 Scope ID。新站点的 site.id 就是 (site, scope_id) 中的 scope_id。

sequenceDiagram
    participant C as SiteController
    participant S as SiteService
    participant SR as SiteRepository
    participant DB as MySQL

    C->>S: CreateSite(request)
    S->>S: 校验父站点、类别、名称和排序位置
    S->>SR: 创建site
    SR->>DB: INSERT site
    DB-->>SR: siteID
    SR-->>S: site
    S-->>C: site

6.2 站点删除和授权清理时序

站点删除必须先完成业务规则和操作者权限校验,再由统一 Grant 服务清理 (site, siteID) 的直接授权绑定。站点插件不得直接写 ums_grant_scope 或 ums_user_role_grant。

sequenceDiagram
    participant C as SiteController
    participant S as SiteService
    participant A as AuthorizationService
    participant U as UnitOfWork
    participant SR as SiteRepository
    participant G as GrantService
    participant DB as MySQL

    C->>S: DeleteSite(siteID)
    S->>SR: 查询site
    SR->>DB: SELECT site
    S->>S: 校验非根站点、无子站点、无业务引用
    S->>A: Check(actor, site:delete, type=site, scopeID=siteID)
    A-->>S: Allowed
    S->>U: WithinTransaction
    U->>SR: 锁定site并复查删除条件
    SR->>DB: SELECT site FOR UPDATE
    U->>G: RemoveScopeBindings(type=site, scopeID=siteID, actor)
    G->>DB: 锁定并软删除该资源的直接Grant-Scope
    G->>DB: 检查受影响Grant的其他有效Scope绑定
    G->>DB: 软删除已无其他绑定的SCOPE_RELATED Grant
    U->>SR: 软删除site
    SR->>DB: UPDATE site SET deleted_at=...
    U->>DB: COMMIT
    S-->>C: 204

删除资源时,只在某个 SCOPE_RELATED Grant 已不存在任何其他有效绑定后才软删除该 ums_user_role_grant。若站点与授权表无法共享本地事务,则使用 Outbox 和幂等清理;清理完成前应禁止删除完成或将资源置为不可授权状态,避免留下指向不存在资源的有效绑定。

7. 统一服务接口

统一授权插件可拆成多个小接口注册为 SPI,但由同一个插件实现和拥有数据。

go
package authorization import "context" type Actor struct { UserID uint64 RequestID string } type ScopeRef struct { ScopeTypeCode string ScopeID int64 } type ScopeResource struct { ID int64 ParentID *int64 Enabled bool } // ScopeResolver由scope_type_code对应的业务插件实现。 type ScopeResolver interface { Resolve(ctx context.Context, scopeID int64) (ScopeResource, error) ListAncestorIDs(ctx context.Context, scopeID int64) ([]int64, error) // 包含自身 ListDescendantIDs(ctx context.Context, scopeID int64) ([]int64, error) // 包含自身 ListAllIDs(ctx context.Context) ([]int64, error) // 全部有效业务资源 } type ScopeResolverRegistry interface { Register(scopeTypeCode string, resolver ScopeResolver) error Resolver(scopeTypeCode string) (ScopeResolver, error) } type PermissionDefinition struct { ApplicationCode string ParentCode *string Code string Name string PermissionType string ScopeRequirement string ScopeTypeCode *string Sort uint64 } type Permission struct { ID uint64 ApplicationCode string ParentID *uint64 Code string Name string PermissionType string ScopeRequirement string ScopeTypeCode *string Sort uint64 Status uint8 } type PermissionService interface { RegisterBatch(ctx context.Context, definitions []PermissionDefinition, actor Actor) error List(ctx context.Context, applicationCode string) ([]Permission, error) } type Role struct { ID uint64 ApplicationCode string Code string Name string ScopeMode string ScopeTypeCode *string Status uint8 } type CreateRoleCommand struct { ApplicationCode string Code string Name string ScopeMode string ScopeTypeCode *string Actor Actor } type RoleFilter struct { ApplicationCode string ScopeMode string ScopeTypeCode *string EnabledOnly bool } type RoleService interface { Create(ctx context.Context, command CreateRoleCommand) (Role, error) UpdateName(ctx context.Context, roleID uint64, name string, actor Actor) error ReplacePermissions(ctx context.Context, roleID uint64, permissionIDs []uint64, actor Actor) error ListAssignable(ctx context.Context, filter RoleFilter) ([]Role, error) Disable(ctx context.Context, roleID uint64, actor Actor) error Delete(ctx context.Context, roleID uint64, actor Actor) error } type ReplaceScopeRolesCommand struct { UserID uint64 Scope ScopeRef TargetRoleIDs []uint64 // 当前Scope编辑后的完整直接角色集合;空数组表示全部移除 Actor Actor } type GrantChange struct { KeptRoleIDs []uint64 AddedRoleIDs []uint64 RemovedRoleIDs []uint64 } type ScopeMember struct { UserID uint64 RoleIDs []uint64 SourceScope ScopeRef Direct bool } type GrantService interface { ReplaceScopeRoles(ctx context.Context, command ReplaceScopeRolesCommand) (GrantChange, error) ReplaceGlobalRoles(ctx context.Context, userID uint64, roleIDs []uint64, actor Actor) (GrantChange, error) ListDirectRoles(ctx context.Context, userID uint64, scope ScopeRef) ([]Role, error) ListScopeMembers(ctx context.Context, scope ScopeRef, includeInherited bool) ([]ScopeMember, error) RemoveScopeBindings(ctx context.Context, scope ScopeRef, actor Actor) error } type CheckRequest struct { UserID uint64 ApplicationCode string PermissionCode string TargetScope *ScopeRef // Scope无关权限传nil } type Decision struct { Allowed bool GrantID *uint64 // 仅用于内部审计和排障,不作为前端授权依据 } type AccessibleScopeRequest struct { UserID uint64 ApplicationCode string PermissionCode string ScopeTypeCode string } type AuthorizationService interface { Check(ctx context.Context, request CheckRequest) (Decision, error) ListAccessibleScopeIDs(ctx context.Context, request AccessibleScopeRequest) ([]int64, error) }

接口规则:

  • ReplaceScopeRoles 只接受 SCOPE_RELATED 且 Scope 类型匹配的角色。
  • ReplaceGlobalRoles 只接受 ALL 或 SCOPE_INDEPENDENT 角色,不写 ums_grant_scope。
  • 角色 ID 由授权服务查库校验;ScopeRef 由 scope_type_code 对应的 Resolver 校验,不能只相信前端候选列表。
  • ListDirectRoles 不返回从祖先 Scope 继承的角色。
  • ListScopeMembers(includeInherited=true) 可以展示继承人员,但继承项必须标注来源 Scope 且不可在子级直接撤销。
  • RemoveScopeBindings 只能清理指定 (scope_type_code, scope_id) 的直接绑定,不能删除业务资源。
  • 所有变更接口校验操作者权限并写审计事件。

7.1 GrantService.RemoveScopeBindings 内部契约

业务插件删除资源前调用 RemoveScopeBindings。它必须加入调用方当前事务,并在同一事务中按以下顺序处理:

  1. 根据 scope_type_code 取得 Resolver,确认目标业务资源仍存在且类型正确;无对应 Resolver 时失败关闭。
  2. 业务插件负责在调用前校验无有效子资源及其他业务引用,授权服务不替代这些业务校验。
  3. 查询并锁定指定 (scope_type_code, scope_id) 的全部有效直接 ums_grant_scope,收集受影响的 grant_id。
  4. 将这些 Grant-Scope 绑定设置为撤销状态,并填写 revoked_by、revoked_at、deleted_at;继承授权没有当前资源的直接绑定,因此不生成或删除继承记录。
  5. 对受影响 Grant 检查是否仍存在任意类型的其他有效 ums_grant_scope 绑定。
  6. 仅当角色的 scope_mode=SCOPE_RELATED 且 Grant 已无任何有效 Scope 绑定时,才撤销并软删除对应 ums_user_role_grant;仍绑定其他资源的 Grant 必须保留。
  7. 记录 Grant-Scope 和 Grant 审计事件,并失效该类型的范围展开缓存及受影响用户的权限缓存。
  8. 任一步骤失败都返回错误,由外层事务回滚授权表和业务表的全部变更。

RemoveScopeBindings 明确不得删除或修改以下数据:业务资源本身、父资源上产生的继承授权、同一 Grant 在其他 Scope 上的绑定、用户、角色、角色权限以及无关业务对象。上述授权清理规则只能由统一授权插件实现,业务插件不得复制。

8. Scope 人员角色的差量更新

ReplaceScopeRoles 以“当前 Scope 的直接角色集合”为基准计算差异,不读取其他 Scope 的角色,也不把继承角色作为可删除项。

sequenceDiagram
    participant C as SiteOperatorController
    participant S as SiteService
    participant A as AuthorizationService
    participant G as GrantService
    participant DB as 授权数据库

    C->>S: PUT /sites/{id}/operators/{userId}\nroleIds=[R2,R3]
    S->>S: 校验站点
    S->>A: Check(actor, site:operator:assign, type=site, scopeID=siteID)
    A-->>S: Allowed
    S->>G: ReplaceScopeRoles(userID, type=site, scopeID=siteID, [R2,R3])
    G->>DB: 锁定当前直接Grant和绑定
    G->>G: current=[R1,R2]\nkeep=[R2], add=[R3], remove=[R1]
    G->>DB: 查找或新增(user,R3) Grant
    G->>DB: 新增(GrantR3,site,siteID)
    G->>DB: 软删除(GrantR1,site,siteID)
    G->>DB: R1若无其他有效Scope则软删除GrantR1
    G->>DB: COMMIT并写审计
    G-->>S: GrantChange
    S-->>C: 200

事务规则:

  1. 按角色 ID 排序锁定,降低并发死锁概率。
  2. 当前集合只查询 scope_type_code=当前类型 AND scope_id=当前业务ID 的有效直接绑定。
  3. 新增角色时复用仍然有效的 (user_id, role_id) Grant;不存在则新增。
  4. 删除角色时只软删除当前 (grant_id, scope_type_code, scope_id),不得修改其他 Scope 绑定。
  5. 某个 SCOPE_RELATED Grant 不再有任何有效 Scope 时,软删除该 Grant。
  6. TargetRoleIDs=[] 表示移除当前 Scope 的全部直接角色;用户账号和其他 Scope 授权保留。
  7. 唯一索引冲突时重新读取有效记录,不得生成两条有效 Grant。

9. 鉴权和列表过滤

9.1 单个目标鉴权

业务接口必须声明权限编码,并把目标业务对象映射为 ScopeRef{ScopeTypeCode, ScopeID}。统一鉴权服务按以下顺序处理:

  1. 用户、Grant、角色、角色权限和权限均有效且未软删除。
  2. 角色和权限属于请求指定的应用。
  3. Scope 无关权限只能通过 ALL 或 SCOPE_INDEPENDENT 角色。
  4. Scope 相关权限:
    • 根据 scope_type_code 调用 Resolver,确认目标业务资源及其祖先有效,并取得包含自身的祖先业务 ID;
    • ALL 角色不查询 ums_grant_scope,覆盖该类型全部有效业务资源;
    • SCOPE_RELATED 角色必须从同一 Grant 找到类型相同且 scope_id 命中目标或祖先业务 ID 的直接绑定。
  5. 找不到 Resolver、目标资源不存在、目标或任一祖先被禁用或软删除时拒绝。
  6. 任一完整 Grant 通过即允许,否则拒绝。

授权表查询示例位于六表参考 DDL末尾。祖先 ID 必须先由业务 Resolver 返回,授权插件无法只查询六张授权表完成业务树递归。

9.2 列表查询

列表接口应先调用:

go
scopeIDs, err := authorization.ListAccessibleScopeIDs(ctx, AccessibleScopeRequest{ UserID: currentUserID, ApplicationCode: "operations", PermissionCode: "site:list", ScopeTypeCode: "site", })

ListAccessibleScopeIDs 通过 Resolver 将直接绑定展开为后代业务 ID;存在 ALL Grant 时调用 ListAllIDs。返回值可以直接用于对应业务表的主键过滤:

sql
SELECT site.* FROM site WHERE site.id IN (?) AND site.deleted_at IS NULL ORDER BY site.sort, site.id;

不能查询全部业务数据后在 Controller 逐条调用 Check,否则会产生 N+1 查询并可能在分页后过滤出错误的 total。数据量较大时,ListAccessibleScopeIDs 可返回临时表、查询条件对象或使用授权缓存;其语义保持不变。

10. 实际案例

10.1 角色和权限配置

创建角色“站点查看人员”:

text
application_code = operations scope_mode = SCOPE_RELATED scope_type_code = site

配置唯一权限:

text
permission.code = site:list permission.scope_requirement = SCOPE_RELATED permission.scope_type_code = site

因为该角色没有 site:update、site:delete 或 site:operator:assign,即使在授权范围内也只能查询站点。

10.2 北京站点授权

Scope 树:

text
根站点 S1 ├─ 北京站点 S2 │ └─ 北京机房 S4 └─ 上海站点 S3

授权数据:

text
ums_user_role_grant G1 张三 站点查看人员 ums_grant_scope G1 site S2(北京站点业务ID)

结果:

请求是否允许原因
列出北京站点是目标等于绑定 Scope
列出北京机房是S4 是 S2 的后代
列出上海站点否S3 是兄弟 Scope
列出根站点否授权不向父级扩展
编辑北京站点否角色没有 site:update

如果前端为了构造导航树需要显示“根站点 → 北京站点”,可以额外返回只用于路径展示的祖先节点并标注 pathOnly=true,不能因此赋予根站点的 site:list 权限。

10.3 同一角色绑定两个站点

张三又在上海站点获得相同角色时:

text
ums_user_role_grant G1 张三 站点查看人员 ums_grant_scope G1 site S2(北京站点业务ID) G1 site S3(上海站点业务ID)

不会新增第二条有效 (张三, 站点查看人员) Grant,只新增一条 Grant-Scope 绑定。

从北京站点移除该角色时,只软删除 G1+S2。因为 G1+S3 仍有效,G1 保持有效;张三仍能查看上海站点。

10.4 站点运营人员差量编辑

北京站点当前直接角色:

text
[站点管理员 R1, 运维人员 R2]

前端提交目标集合:

json
{ "roleIds": [2, 3] }

统一 Grant 服务计算:

text
保留:运维人员 R2 新增:业务运营人员 R3 删除:站点管理员 R1

只写入 R3 的新增绑定并软删除 R1 的北京站点绑定,R2 的原记录和创建时间保持不变。

前端提交:

json
{ "roleIds": [] }

表示移除张三在北京站点的全部直接角色。北京站点人员列表不再显示张三;来自父站点的继承角色和张三在上海站点的绑定不受影响。

10.5 通讯录和组网复用

通讯录插件直接在自己的业务表中创建“研发部”,不调用授权服务创建 Scope。插件启动时注册:

text
ScopeResolverRegistry.Register("contact", contactScopeResolver)

给“研发部”添加人员时,统一授权插件通过 contactScopeResolver 校验通讯录节点,并写入:

text
ums_grant_scope(grant_id=G2, scope_type_code=contact, scope_id=研发部业务ID)

组网插件注册 network Resolver 并使用组网业务 ID。通讯录管理员不能绑定到站点 Scope,原因是角色与目标 scope_type_code 不同。各插件复用同一套 Grant 差量逻辑,无需各自实现用户角色授权表。

11. 一致性、缓存和审计

11.1 一致性

  • 创建和改名只修改业务表,不写授权表。
  • 业务资源删除与直接 Grant-Scope 清理必须使用同一事务或可靠 Outbox。
  • Grant 差量更新必须在授权插件的单个数据库事务中完成。
  • 服务端最终校验不能被前端角色候选过滤替代。
  • 业务资源移动时不改写 Grant-Scope 绑定;权限范围通过 Resolver 按新业务树重算。
  • Resolver 缺失、调用失败或找不到业务资源时鉴权失败关闭,不得按全局权限放行。

11.2 缓存

可缓存用户权限和 Scope 展开结果,但以下变化必须失效相关缓存:

text
角色权限变化 角色状态变化 用户状态变化 Grant或Grant-Scope变化 业务资源父节点、状态或删除状态变化

缓存只用于加速。授权关系以六张授权表为准;资源存在性、状态和层级以对应业务表及 Resolver 为准。

11.3 审计

角色权限、用户角色 Grant 和 Grant-Scope 绑定的每次变化由统一授权插件记录;业务资源生命周期由对应业务插件记录:

text
request_id、actor_id、动作、目标类型、目标ID、变更前、变更后、时间、结果

软删除字段用于业务有效性和历史关联,不替代审计日志。

12. 错误语义

统一服务至少区分以下错误:

错误含义
AUTH_USER_NOT_FOUND用户不存在、已删除或不可用
AUTH_ROLE_NOT_FOUND角色不存在或已删除
AUTH_ROLE_SCOPE_MISMATCH角色模式或 Scope 类型不匹配
AUTH_PERMISSION_INCOMPATIBLE角色与权限的范围模式不兼容
AUTH_SCOPE_NOT_FOUNDScope 不存在或已删除
AUTH_SCOPE_DISABLEDScope 或其祖先被禁用
AUTH_SCOPE_RESOLVER_NOT_FOUND未注册目标 scope_type_code 的 Resolver
AUTH_FORBIDDEN操作人没有目标权限或目标范围
AUTH_CONFLICT并发修改或有效唯一约束冲突且无法重试

业务插件可以将统一错误映射成自己的稳定 HTTP 错误码,但不能把授权失败转换为成功或忽略。

13. 验收场景

场景预期
创建站点只新增站点业务记录,不写任何授权表
未注册目标类型的 Resolver鉴权和授权绑定均失败关闭
site.id=1 与 contact_group.id=1 同时授权通过不同 scope_type_code 分别保存,不冲突
同一用户、角色绑定北京和上海一条有效 Grant、两条有效 Grant-Scope
北京站点移除角色,上海仍绑定仅北京绑定软删除,Grant 保持有效
删除最后一个直接绑定绑定和无其他范围的 Scope 相关 Grant 均软删除
子站点尝试删除父站点继承角色拒绝;只能在直接授权来源 Scope 操作
北京站点授权查询北京机房允许
北京站点授权查询上海站点拒绝
只有 site:list 时编辑北京站点拒绝
禁用根站点后查询北京站点拒绝,即使北京站点自身仍为启用
通讯录角色绑定站点 Scope拒绝,Scope 类型不匹配
ALL角色仅配置site:list可查看全部有效站点,但不能编辑或删除站点
历史绑定软删除后重新授权插入新的有效记录;历史记录保留且有效唯一索引不冲突

14. 实施前置事项

当前 Aurora RBAC 已有 t_user、t_role、t_role_permission 等表。实施前必须确定唯一数据源方案:

  1. 扩展 Aurora RBAC,使其表和接口承载本模型;或
  2. 将 Aurora 身份和角色数据迁移到本模型,由统一授权插件成为唯一权威数据源。

不得让 Aurora 表和 ums_* 表长期同时接受写入,否则用户状态、角色权限和授权判断会发生漂移。迁移方案、数据回填、兼容期读写策略和回滚方案需要单独形成迁移设计。

本文作者:枣子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!