本教程将以「Forge Admin」在线演示环境为例,手把手带你完成从用户创建、角色配置、菜单授权、按钮权限、API 权限到数据权限的全链路配置。
Forge Admin 采用 RBAC(基于角色的访问控制) 权限模型,核心关系链如下:
用户 ──> 角色 ──> 资源(菜单/按钮/API)
│
├── 菜单权限:控制侧边栏能看到什么页面
├── 按钮权限:控制页面上能看到什么操作按钮
├── API 权限:控制后端接口能否被调用
└── 数据权限:控制查询结果能看到哪些数据
| 表名 | 说明 |
|---|---|
sys_user |
用户表,存储用户基本信息 |
sys_role |
角色表,定义角色和数据权限范围 |
sys_user_role |
用户-角色关联表(多对多) |
sys_resource |
资源表,存储菜单、按钮、API 三类资源 |
sys_role_resource |
角色-资源关联表(多对多) |
sys_resource.resource_type 字段定义了四种资源类型:
| 值 | 类型 | 说明 |
|---|---|---|
| 1 | 目录 | 侧边栏的分组目录,如「系统管理」 |
| 2 | 菜单 | 实际页面路由,如「用户管理」 |
| 3 | 按钮 | 页面内的操作按钮,如「新增」「删除」 |
| 4 | API 接口 | 后端接口地址,用于接口级权限控制 |
用户请求 ──> Sa-Token 登录校验 ──> API 权限拦截器 ──> 业务逻辑
│
├── 检查接口是否配置了 API 资源
├── 如果配置了,检查用户 API 权限列表
└── 通配符匹配(如 /system/**)
前端路由守卫会在页面跳转时检查用户是否有该路由的访问权限,未授权的页面会跳转到 403。
admin 账号在弹出的表单中填写以下信息:
zhangsan张三系统管理员(type=0):拥有全部权限,不受数据权限限制租户管理员(type=1):管理本租户内的用户和配置普通用户(type=2):只能访问被授权的资源点击 确定 保存
| 用户类型 | userType 值 | 权限特点 |
|---|---|---|
| 系统管理员 | 0 | 超级管理员,isAdmin() 返回 true,API 权限含 /** 通配符,直接放行所有接口 |
| 租户管理员 | 1 | isTenantAdmin() 返回 true,管理本租户资源 |
| 普通用户 | 2 | 严格按照角色绑定的资源进行权限控制 |
::: tip 重要提示
系统管理员(userType=0)在 PermissionServiceImpl 中会直接匹配 /** 通配符,跳过所有 API 权限检查。请谨慎分配此类型。
:::
| 字段 | 说明 | 示例 |
|---|---|---|
| 角色名称 | 租户内唯一的角色名称 | 人事专员 |
| 角色标识 | 权限字符串,用于代码中权限判断 | hr_staff |
| 角色类型 | 管理角色/业务角色/审批角色/数据角色 | 业务角色 |
| 数据权限范围 | 决定该角色能看到哪些数据 | 本人数据 |
| 排序 | 数值越小越靠前 | 1 |
| 状态 | 启用/禁用 | 正常 |
SysRole.dataScope 字段定义了角色的数据可见范围:
| 值 | 类型 | 说明 | 效果 |
|---|---|---|---|
| 1 | 全部数据 | ALL |
不追加任何数据过滤条件,看到所有数据 |
| 2 | 本租户数据 | TENANT_ALL |
追加 tenant_id = 当前租户ID 条件 |
| 3 | 本组织数据 | ORG |
追加 org_id IN (用户所属组织ID) 条件 |
| 4 | 本组织及子组织 | ORG_AND_CHILD |
追加 org_id IN (用户组织+子组织ID列表) 条件 |
| 5 | 个人数据 | SELF |
追加 create_by = 当前用户ID 条件 |
| 7 | 本行政区划 | REGION |
按行政区划编码过滤(省级不限,市级及以下过滤本级+下级) |
::: warning 数据权限多角色取最小范围 一个用户绑定了多个角色时,系统会取所有角色中最严格的数据权限范围(即范围最小的那个)。例如:角色A是「本组织」,角色B是「全部数据」,最终生效的是「本组织」。 :::
资源树结构示例:
├── 系统管理(目录)
│ ├── 用户管理(菜单)
│ │ ├── 新增(按钮,perms: sys:user:add)
│ │ ├── 编辑(按钮,perms: sys:user:edit)
│ │ ├── 删除(按钮,perms: sys:user:remove)
│ │ └── 导出(按钮,perms: sys:user:export)
│ ├── 角色管理(菜单)
│ │ ├── 新增(按钮)
│ │ └── ...
│ └── ...
├── AI 大屏(目录)
│ └── ...
一个用户可以同时绑定多个角色,权限取并集,数据权限取最小范围。
目录是侧边栏的分组容器,如「系统管理」「AI 大屏」。
| 字段 | 说明 | 示例 |
|---|---|---|
| 资源名称 | 目录显示名称 | 人事管理 |
| 父级资源 | 顶级目录选「根目录」 | 根目录 |
| 排序 | 数值越小越靠前 | 10 |
| 图标 | 目录图标 | md-people |
| 显示状态 | 是否在侧边栏显示 | 显示 |
菜单是实际的页面路由,对应前端的一个 Vue 组件。
| 字段 | 说明 | 示例 |
|---|---|---|
| 资源名称 | 菜单显示名称 | 员工列表 |
| 父级资源 | 选择所属目录 | 人事管理 |
| 资源路由 | 前端路由路径 | /employee/list |
| 前端组件 | Vue 组件路径 | employee/index |
| 权限标识 | 按钮权限用,菜单可留空 | employee:list |
| 图标 | 菜单图标 | md-list |
| 是否缓存 | 是否keep-alive | 是 |
| 最低用户类型 | 限制访问的用户类型 | 普通用户 |
::: tip 路由与组件映射
前端使用 unplugin-vue-router 自动路由,组件路径相对于 src/views/ 目录。例如 employee/index 对应 src/views/employee/index.vue。
:::
按钮权限控制页面内的操作按钮可见性。
| 字段 | 说明 | 示例 |
|---|---|---|
| 资源名称 | 按钮显示名称 | 新增员工 |
| 父级资源 | 选择所属菜单 | 员工列表 |
| 权限标识 | 关键字段,前端用此标识判断按钮显示 | employee:add |
API 权限控制后端接口的访问,是最高级别的权限控制。
| 字段 | 说明 | 示例 |
|---|---|---|
| 资源名称 | 接口描述 | 新增员工接口 |
| 父级资源 | 选择所属菜单 | 员工列表 |
| API 请求方法 | HTTP 方法 | POST |
| API 接口地址 | 接口路径,支持通配符 | /employee/add |
::: warning API 权限工作原理
后端 ApiPermissionInterceptor 拦截器会在每次请求时:
sys_resource 中配置了 API 类型资源apiPermissions 列表PathMatcher 通配符匹配,如 /system/** 匹配 /system/user/add所以只有配置了 API 资源的接口才会被拦截。建议对所有敏感接口都配置 API 资源。 :::
某些接口不需要登录即可访问(如登录接口、验证码接口),可以通过以下方式跳过权限校验:
方式一:设置公开资源
在资源管理中,将 isPublic 字段设为 1,该资源无需权限验证。
方式二:后端注解
// 使用 @SaIgnore 注解,跳过 Sa-Token 登录校验和 API 权限校验
@SaIgnore
@GetMapping("/public/info")
public RespInfo<String> publicInfo() {
return RespInfo.success("公开信息");
}
// 使用 @ApiPermissionIgnore 注解,仅跳过 API 权限校验(仍需登录)
@ApiPermissionIgnore
@GetMapping("/internal/info")
public RespInfo<String> internalInfo() {
return RespInfo.success("内部信息");
}
方式三:API 配置管理
在「系统管理 → API 配置管理」中,将接口的 needAuth 设为 false,该接口可匿名访问。
前端路由守卫 permission-guard.js 会在每次路由跳转时检查:
无权限访问的页面会自动跳转到 /403。
在前端页面中使用 v-permission 指令控制按钮显示:
<template>
<n-button v-permission="'employee:add'" type="primary">新增</n-button>
<n-button v-permission="'employee:edit'" type="warning">编辑</n-button>
<n-button v-permission="'employee:remove'" type="error">删除</n-button>
</template>
只有当前用户角色中包含对应 perms 标识的资源,按钮才会显示。
import { useUserStore } from '@/store'
const userStore = useUserStore()
// 判断是否有某个权限
const canAdd = userStore.hasPermission('employee:add')
// 判断是否是管理员
const isAdmin = userStore.userInfo.userType === 0
在角色管理中,每个角色都有一个 dataScope 字段,定义该角色的数据可见范围(见 3.3 数据权限范围详解)。
对于更细粒度的数据权限控制,Forge Admin 提供了 sys_data_scope_config 配置表,可以按 Mapper 方法级别配置:
com.mdframe.forge.employee.mapper.EmployeeMapper.selectPageecreate_bydept_idtenant_id数据权限通过 MyBatis-Plus 的 InnerInterceptor 实现,在 SQL 执行前自动改写 WHERE 条件:
-- 原始 SQL
SELECT * FROM sys_employee WHERE del_flag = 0
-- 用户角色 dataScope=5(本人数据),自动改写为:
SELECT * FROM sys_employee WHERE del_flag = 0 AND create_by = 1
-- 用户角色 dataScope=3(本组织数据),自动改写为:
SELECT * FROM sys_employee WHERE del_flag = 0 AND dept_id IN (100, 101)
对于复杂的业务场景,支持自定义 SQL 条件:
// 在 SysDataScopeConfig 中配置自定义 SQL
// 使用 <sql> 标签包裹,支持占位符:#{userId}、#{tenantId}、#{orgIds}
String customSql = "<sql>e.dept_id IN (SELECT dept_id FROM sys_user_dept WHERE user_id = #{userId})</sql>";
某些场景需要跳过数据权限(如后台任务、数据同步):
// 方式一:DataScopeContextHolder 设置跳过标记
DataScopeContextHolder.setSkip(true);
try {
// 此查询不会追加数据权限条件
List<Employee> all = employeeMapper.selectList(null);
} finally {
DataScopeContextHolder.setSkip(false);
}
// 方式二:@DataScopeIgnore 注解(如有)
Forge Admin 的多租户隔离基于 MyBatis-Plus 的 TenantLineInnerInterceptor 实现:
TenantEntity 的实体类,其表会自动追加 tenant_id 条件TenantContextHolder 中获取,在用户登录时自动设置sys_resource(资源表)也继承 TenantEntity,意味着每个租户可以有自己的资源树| 操作 | 系统管理员 | 租户管理员 | 普通用户 |
|---|---|---|---|
| 创建租户 | ✅ | ❌ | ❌ |
| 管理本租户用户 | ✅ | ✅ | ❌ |
| 配置本租户角色 | ✅ | ✅ | ❌ |
| 配置本租户菜单 | ✅ | ✅ | ❌ |
| 跨租户操作 | ✅ | ❌ | ❌ |
| 查看全部数据 | ✅ | ❌ | ❌ |
// 方式一:注解
@IgnoreTenant
public List<SysTenant> listAllTenants() {
return tenantMapper.selectList(null);
}
// 方式二:工具类
List<SysTenant> tenants = TenantContextHolder.executeIgnore(() -> {
return tenantMapper.selectList(null);
});
需求:人事部门的专员可以管理员工信息,只能看到本部门员工数据,不能删除员工。
步骤:
| 字段 | 值 |
|---|---|
| 角色名称 | 人事专员 |
| 角色标识 | hr_staff |
| 角色类型 | 业务角色 |
| 数据权限范围 | 本组织数据(3) |
勾选以下资源:
employee:add)employee:edit)employee:list)employee:export)employee:remove)— 不勾选在数据权限配置中新增:
com.mdframe.forge.employee.mapper.EmployeeMapper.selectPageedept_idlisi(李四),所属组织选择「人事部」lisi使用 lisi 账号登录后:
/employee/remove/{id} 接口会返回 403 无权限排查步骤:
visible 和 menuStatus 是否为显示状态minUserType 是否限制了用户类型排查步骤:
sys_resource 中配置了 API 类型资源apiPermissions 列表(可在 Redis 中查看 Sa-Token Session)forge.auth.enable-api-permission 配置是否开启排查步骤:
sys_data_scope_config 中已配置dataScope 字段值是否正确DataScopeContextHolder 是否设置了跳过标记DataScopeInterceptor 的 SQL 改写记录超级管理员(userType=0)的特殊待遇:
/**,直接放行所有接口ALL,不追加任何数据过滤条件minUserType 限制,可访问所有菜单取最严格的范围。例如: