本文是一套 可落地、可扩展、符合银行/金融系统实践 的后台权限设计方案,基于 RBAC(基于角色的访问控制),并完整覆盖:
- API 权限
- 菜单 / 按钮权限
- 数据权限(含 CUSTOM 自定义)
一、设计背景与目标
1.1 业务背景
后台管理系统(银行 / 贷款 / 金融 / 审批类系统)通常具备以下共性:
- 多角色、多岗位(管理员、信贷员、风控、负责人等)
- 接口一致,但数据可见范围不同
- 菜单、按钮、接口权限需要统一管理
- 存在跨部门、临时授权、区域授权等复杂场景
如果没有一套清晰的权限模型,系统将很快出现:
- 权限规则混乱
- SQL 到处写 if/else
- 数据越权风险
- 新需求难以扩展
1.2 设计目标
本方案旨在实现:
- 统一、标准的权限模型
- 结构清晰、可维护的表设计
- API 作为安全兜底
- 灵活但可控的数据权限
- 支持复杂业务的自定义授权(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 解决现实业务中的例外情况
这是一套 真正能在银行 / 金融系统中长期演进的后台权限设计方案。