本文是一套 可落地、可扩展、符合银行/金融系统实践 的后台权限设计方案,基于 RBAC(基于角色的访问控制),并完整覆盖:

  • API 权限
  • 菜单 / 按钮权限
  • 数据权限(含 CUSTOM 自定义)

一、设计背景与目标

1.1 业务背景

后台管理系统(银行 / 贷款 / 金融 / 审批类系统)通常具备以下共性:

  • 多角色、多岗位(管理员、信贷员、风控、负责人等)
  • 接口一致,但数据可见范围不同
  • 菜单、按钮、接口权限需要统一管理
  • 存在跨部门、临时授权、区域授权等复杂场景

如果没有一套清晰的权限模型,系统将很快出现:

  • 权限规则混乱
  • SQL 到处写 if/else
  • 数据越权风险
  • 新需求难以扩展

1.2 设计目标

本方案旨在实现:

  1. 统一、标准的权限模型
  2. 结构清晰、可维护的表设计
  3. API 作为安全兜底
  4. 灵活但可控的数据权限
  5. 支持复杂业务的自定义授权(CUSTOM)

二、整体权限模型概览

2.1 权限模型选型

采用业内事实标准:

RBAC(Role-Based Access Control)

并在 RBAC 基础上拆分权限层次:

  • API 权限(是否能访问接口)
  • 菜单权限(页面是否可见)
  • 按钮权限(操作是否可点)
  • 数据权限(能看到哪些数据行)

2.2 权限分层思想(非常重要)

层级 控制内容 是否真正安全 说明
API 权限 接口是否可访问 后端兜底,必须校验
菜单权限 页面是否显示 仅用于前端展示
按钮权限 按钮是否显示 仅用于前端控制
数据权限 能看到哪些数据 决定 SQL WHERE

真正的安全 = API 权限 + 数据权限
菜单 / 按钮只负责“看不看得到”,不负责“能不能做”。

三、数据表 ER 图设计

3.1 表结构与关系

erDiagram
		sys_admin ||--o{ sys_admin_role : "一对多"
    sys_role  ||--o{ sys_admin_role : "一对多"
    sys_role ||--o{ sys_role_permission : "一对多"
    sys_permission ||--o{ sys_role_permission : "一对多"
    sys_dept ||--o{ sys_admin : "一对多"

    sys_admin {
        int id PK "用户ID"
        varchar name "姓名"
        char password "密码(MD5/加密)"
        char mobile "手机号"
        int dept_id FK "所属部门ID"
        datetime register_time "注册时间"
        datetime last_time "最后登录时间"
        varchar last_ip "最后登录IP"
        tinyint is_delete "是否删除(0否1是)"
        datetime created_at "创建时间"
        datetime updated_at "更新时间"
    }

    sys_dept {
        int id PK "部门ID"
        int parent_id FK "父部门ID"
        varchar name "部门名称"
        datetime created_at "创建时间"
        datetime updated_at "更新时间"
    }

    sys_role {
        int id PK "角色ID"
        varchar name "角色名称"
        varchar scope "数据权限范围(ALL/DEPT/DEPT_AND_SUB/SELF/CUSTOM)"
        datetime created_at "创建时间"
        datetime updated_at "更新时间"
    }

    sys_permission {
        int id PK "权限ID"
        int parent_id FK "父级权限ID(菜单用)"
        tinyint type "权限类型(1接口 2菜单 3按钮)"
        varchar name "权限名称"
        varchar method "HTTP方法(GET/POST等)"
        varchar path "接口或路由路径"
        int sort "排序"
        datetime created_at "创建时间"
        datetime updated_at "更新时间"
    }

    sys_admin_role {
        int id PK "主键ID"
        int admin_id FK "后台用户ID"
        int role_id FK "角色ID"
        datetime created_at "创建时间"
        datetime updated_at "更新时间"
    }

    sys_role_permission {
        int id PK "主键ID"
        int role_id FK "角色ID"
        int permission_id FK "权限ID"
        datetime created_at "创建时间"
        datetime updated_at "更新时间"
    }

3.2 表说明速览

表名 作用
sys_admin 后台用户
sys_dept 部门组织
sys_role 角色(承载权限 + 数据范围)
sys_permission 权限(API / 菜单 / 按钮)
sys_admin_role 用户-角色关联
sys_role_permission 角色-权限关联

四、RBAC 核心设计说明

4.1 为什么“用户不直接绑定权限”

  • 一个用户可能有多个角色
  • 角色才是业务语义的载体
  • 权限集中在角色上,便于维护和审计

用户 → 角色 → 权限
永远不要:用户 → 权限

4.2 sys_permission 的设计思想

权限类型

1 = API 权限
2 = 菜单权限
3 = 按钮权限

权限 code 设计原则

  • 前后端统一
  • 语义清晰
  • 可复用

示例:

loan:create
loan:approve
loan:grant
loan:post:list

五、系统中如何使用这套权限

5.1 API 权限(中间件,安全兜底)

func PermissionMiddleware(r *ghttp.Request) {
    user := GetLoginUser(r.Context())

    ok := permissionSvc.HasApiPermission(
        user.Id,
        r.URL.Path,
        r.Method,
    )

    if !ok {
        r.Response.WriteStatusExit(403, "无权限访问")
    }

    r.Middleware.Next()
}

核心校验逻辑

SELECT 1
FROM sys_admin_role ur
JOIN sys_role_permission rp ON ur.role_id = rp.role_id
JOIN sys_permission p ON rp.permission_id = p.id
WHERE ur.admin_id = ?
  AND p.type = 'api'
  AND p.path = ?
  AND p.method = ?

5.2 菜单权限(登录后返回)

SELECT p.*
FROM sys_admin_role ur
JOIN sys_role_permission rp ON ur.role_id = rp.role_id
JOIN sys_permission p ON rp.permission_id = p.id
WHERE ur.admin_id = ?
  AND p.type = 'menu'
ORDER BY p.sort;

后端返回菜单树,前端负责渲染。

5.3 按钮权限(前端控制)

SELECT p.name
FROM sys_admin_role ur
JOIN sys_role_permission rp ON ur.role_id = rp.role_id
JOIN sys_permission p ON rp.permission_id = p.id
WHERE ur.admin_id = ?
  AND p.type = 'button';

前端示例:

<v-button v-if="hasPermission('loan:grant')" />

六、数据权限(Data Scope)设计

6.1 data_scope 的本质

data_scope 决定 SQL 查询时 WHERE 条件如何拼

它只控制:

  • 能看到哪些数据行

它不控制:

  • 接口是否能访问
  • 页面是否显示

6.2 标准 data_scope 枚举

ALL              全部数据
DEPT             本部门
DEPT_AND_SUB     本部门及子部门
SELF             仅本人
CUSTOM           自定义

6.3 数据权限只能放在 Service 层

func (s *LoanService) List(ctx context.Context, user *LoginUser) {
    switch user.DataScope {
    case "ALL":
    case "DEPT":
        // dept_id = user.DeptId
    case "DEPT_AND_SUB":
        // dept_id IN (...)
    case "SELF":
        // creator_id = user.Id
    }
}

数据权限禁止放在中间件或 Controller

七、多角色 data_scope 冲突解决策略

7.1 核心原则

一个用户,在一个业务场景下,只能有一个最终 data_scope

7.2 推荐策略(银行系统首选)

最宽权限优先

ALL > DEPT_AND_SUB > DEPT > CUSTOM > SELF

示例

角色组合 最终 scope
DEPT + SELF DEPT
CUSTOM + DEPT DEPT
SELF + CUSTOM CUSTOM

7.3 登录时计算最终 data_scope

登录 → 查询角色 → 计算 scope → 写入 Session / JWT
func ResolveDataScope(roles []Role) string {
    priority := map[string]int{
        "ALL": 4,
        "DEPT_AND_SUB": 3,
        "DEPT": 2,
        "CUSTOM": 1,
        "SELF": 0,
    }

    max := -1
    result := "SELF"

    for _, r := range roles {
        if p := priority[r.DataScope]; p > max {
            max = p
            result = r.DataScope
        }
    }
    return result
}

八、CUSTOM 自定义数据权限(高级能力)

8.1 什么时候需要 CUSTOM

  • 跨部门授权
  • 区域/片区管理
  • 临时审计授权
  • 精确到具体业务对象

8.2 自定义数据权限表设计

CREATE TABLE sys_role_data_scope (
  id          BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID',
  role_id     BIGINT NOT NULL COMMENT '角色ID',
  scope_type  VARCHAR(32) NOT NULL COMMENT '授权类型',
  scope_id    BIGINT NOT NULL COMMENT '授权对象ID',
  created_at  DATETIME
) COMMENT='角色自定义数据权限表';

scope_type 规范

DEPT        部门
USER        用户
LOAN        贷款
CUSTOMER    客户

8.3 CUSTOM 示例

风控专员角色(CUSTOM)
→ 授权部门:10、12
INSERT INTO sys_role_data_scope (role_id, scope_type, scope_id)
VALUES
(5, 'DEPT', 10),
(5, 'DEPT', 12);

8.4 CUSTOM 在代码中的生效方式

登录态结构

type LoginUser struct {
    Id           int64
    DeptId       int64
    DataScope    string
    CustomScopes map[string][]int64
}

加载 CUSTOM 数据

func LoadCustomScopes(roleIds []int64) map[string][]int64 {
    rows := []struct {
        ScopeType string
        ScopeId   int64
    }{}

    g.DB().Model("sys_role_data_scope").
        WhereIn("role_id", roleIds).
        Scan(&rows)

    result := make(map[string][]int64)
    for _, r := range rows {
        result[r.ScopeType] = append(result[r.ScopeType], r.ScopeId)
    }
    return result
}

8.5 Service 层统一拼接 SQL

func (s *LoanService) applyDataScope(db *gdb.Model, user *LoginUser) *gdb.Model {
    switch user.DataScope {
    case DataScopeAll:
        return db
    case DataScopeDept:
        return db.Where("dept_id", user.DeptId)
    case DataScopeDeptAndSub:
        return db.WhereIn("dept_id", s.deptSvc.GetDeptAndSubIds(user.DeptId))
    case DataScopeSelf:
        return db.Where("creator_id", user.Id)
    case DataScopeCustom:
        deptIds := user.CustomScopes["DEPT"]
        if len(deptIds) == 0 {
            return db.Where("1=0")
        }
        return db.WhereIn("dept_id", deptIds)
    }
    return db
}

九、总结

这套权限方案的核心思想是:

  • RBAC 控制“能不能做”
  • API 权限 作为安全兜底
  • data_scope 控制“能看多少”
  • CUSTOM 解决现实业务中的例外情况

这是一套 真正能在银行 / 金融系统中长期演进的后台权限设计方案