本文是一套 可落地、可扩展、符合银行/金融系统实践 的后台权限设计方案,基于 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
2
3
1 = API 权限
2 = 菜单权限
3 = 按钮权限

权限 code 设计原则

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

示例:

1
2
3
4
loan:create
loan:approve
loan:grant
loan:post:list

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()
}

核心校验逻辑

1
2
3
4
5
6
7
8
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 菜单权限(登录后返回)

1
2
3
4
5
6
7
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 按钮权限(前端控制)

1
2
3
4
5
6
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';

前端示例:

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

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

6.1 data_scope 的本质

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

它只控制:

  • 能看到哪些数据行

它不控制:

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

6.2 标准 data_scope 枚举

1
2
3
4
5
ALL              全部数据
DEPT 本部门
DEPT_AND_SUB 本部门及子部门
SELF 仅本人
CUSTOM 自定义

6.3 数据权限只能放在 Service 层

1
2
3
4
5
6
7
8
9
10
11
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 推荐策略(银行系统首选)

最宽权限优先

1
ALL > DEPT_AND_SUB > DEPT > CUSTOM > SELF

示例

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

7.3 登录时计算最终 data_scope

1
登录 → 查询角色 → 计算 scope → 写入 Session / JWT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 自定义数据权限表设计

1
2
3
4
5
6
7
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 规范

1
2
3
4
DEPT        部门
USER 用户
LOAN 贷款
CUSTOMER 客户

8.3 CUSTOM 示例

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

8.4 CUSTOM 在代码中的生效方式

登录态结构

1
2
3
4
5
6
type LoginUser struct {
Id int64
DeptId int64
DataScope string
CustomScopes map[string][]int64
}

加载 CUSTOM 数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 解决现实业务中的例外情况

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