权限模型
Fastdotnet 采用多层次的权限控制体系,包括 API 权限、菜单权限、按钮权限和数据权限。
🔐 权限体系架构
用户 (User)
↓
角色 (Role)
↓
权限 (Permission)
├─ API 权限 # 接口访问控制
├─ 菜单权限 # 菜单显示控制
├─ 按钮权限 # 操作按钮控制
└─ 数据权限 # 数据范围控制👥 核心概念
1. 用户 (User)
系统的使用者,分为:
- 管理员用户 (
FdAdminUser) - 管理端用户 - 应用用户 (
FdAppUser) - 应用端用户
用户可以分配一个或多个角色。
2. 角色 (Role)
权限的集合体,分为:
- 管理员角色 (
FdAdminRole) - 管理端角色 - 应用角色 (
FdAppRole) - 应用端角色
角色可以分配给多个用户,一个用户可以拥有多个角色。
3. 权限 (Permission)
最小的权限单元,包括:
- API 权限 - 控制接口访问(如
user.read,user.write) - 菜单权限 - 控制菜单显示
- 按钮权限 - 控制操作按钮可见性
- 数据权限 - 控制数据访问范围
4. 菜单 (Menu)
系统功能菜单项,分为:
- 管理端菜单 (
SystemCategory.Admin) - 应用端菜单 (
SystemCategory.App)
菜单与权限关联,控制前端路由和页面访问。
🎯 权限类型详解
1. API 权限
通过 JWT Token 中的 Claims 进行验证:
csharp
[ApiController]
[Route("api/users")]
public class UserController : ControllerBase
{
// 要求 user.read 权限
[HttpGet]
[Authorize(Policy = "user.read")]
public IActionResult GetUsers() { ... }
// 要求 user.write 权限
[HttpPost]
[Authorize(Policy = "user.write")]
public IActionResult CreateUser() { ... }
}2. 菜单权限
用户登录后,根据其角色获取有权限的菜单树:
csharp
// 获取用户菜单树
var menus = await _menuService.GetUserMenuTreeAsync(userId);
// 超级管理员返回所有菜单
if (isSuperAdmin)
{
return allMenus;
}
// 普通用户返回角色关联的菜单
return roleMenus;3. 按钮权限
前端根据用户权限显示/隐藏按钮:
vue
<template>
<!-- 有编辑权限才显示编辑按钮 -->
<el-button v-if="hasPermission('user.edit')" @click="handleEdit">
编辑
</el-button>
<!-- 有删除权限才显示删除按钮 -->
<el-button v-if="hasPermission('user.delete')" @click="handleDelete">
删除
</el-button>
</template>
<script setup>
import { useUserInfo } from '@/stores/userInfo'
const stores = useUserInfo()
const hasPermission = (permission) => {
return stores.userInfos.permissions.includes(permission)
}
</script>4. 数据权限 ⭐
数据权限控制用户可以访问的数据范围,支持多种策略:
数据权限级别
| 级别 | 说明 | 适用场景 |
|---|---|---|
| 全部数据 | 查看所有数据 | 超级管理员 |
| 本部门数据 | 查看本部门数据 | 部门经理 |
| 本部门及子部门 | 查看本部门及下级部门 | 高层管理 |
| 本人数据 | 仅查看自己创建的数据 | 普通员工 |
| 自定义数据 | 按自定义规则过滤 | 特殊需求 |
实现原理
通过 SqlSugar 的表达式树自动注入 WHERE 条件:
csharp
// 数据权限策略接口
public interface IPermissionStrategy<T> where T : class
{
// 查询时自动注入过滤条件
Expression<Func<T, bool>> GetFilterExpression(PermissionContext context);
// 写操作时校验单个实体
Task<bool> CanAccessAsync(T entity, PermissionContext context);
}
// 本人数据策略示例
public class OwnerPermissionStrategy<T> : IPermissionStrategy<T>
where T : class, IHaveOwner
{
public Expression<Func<T, bool>> GetFilterExpression(PermissionContext context)
{
return entity => ((IHaveOwner)entity).CreatedUserId == context.CurrentUser.Id;
}
}
// 使用示例
var query = _db.Queryable<Order>()
.Where(permissionStrategy.GetFilterExpression(context));配置数据权限
在角色管理中为角色配置数据权限:
json
{
"roleId": "role_123",
"dataScope": {
"type": "Department", // 数据权限类型
"departmentIds": ["dept_1", "dept_2"], // 指定部门
"customRules": [] // 自定义规则
}
}🔄 权限验证流程
1. 登录流程
用户登录
↓
验证用户名密码
↓
生成 JWT Token(包含用户ID、角色、权限)
↓
返回 Token + 用户信息 + 菜单树2. API 请求流程
客户端请求
↓
JWT 中间件验证 Token
↓
解析 Claims(用户ID、角色、权限)
↓
Authorize 特性检查权限
↓
允许/拒绝访问3. 菜单加载流程
前端初始化
↓
从 Store 获取用户权限
↓
递归过滤菜单树(setFilterHasRolesMenu)
↓
渲染有权限的菜单📊 数据库表结构
核心表
| 表名 | 说明 | 关键字段 |
|---|---|---|
fd_admin_user | 管理员用户 | Id, UserName, Password |
fd_app_user | 应用用户 | Id, UserName, Password |
fd_role | 角色 | Id, RoleName, DataScope |
fd_menu | 菜单 | Id, Title, Code, Path, Belong |
fd_menu_button | 按钮权限 | Id, MenuCode, ButtonCode |
fd_admin_user_role | 用户角色关联 | UserId, RoleId |
fd_role_menu | 角色菜单关联 | RoleId, MenuId |
fd_role_menu_button | 角色按钮关联 | RoleId, MenuButtonId |
关系图
User ←→ UserRole ←→ Role
↓
RoleMenu → Menu
↓
RoleMenuButton → MenuButton💡 最佳实践
1. 权限命名规范
csharp
// 格式:{模块}.{资源}.{操作}
public static class Permissions
{
public static class User
{
public const string View = "user.view"; // 查看用户
public const string Create = "user.create"; // 创建用户
public const string Edit = "user.edit"; // 编辑用户
public const string Delete = "user.delete"; // 删除用户
}
public static class Order
{
public const string View = "order.view";
public const string Approve = "order.approve";
}
}2. 超级管理员处理
csharp
// 判断是否为超级管理员
public async Task<bool> IsSuperAdminAsync(string userId)
{
var roles = await GetUserRolesAsync(userId);
return roles.Any(r => r.Code == "superadmin");
}
// 超级管理员跳过权限检查
if (isSuperAdmin)
{
return allData; // 返回所有数据
}3. 权限缓存
csharp
// 缓存用户权限(Redis)
var cacheKey = $"user:permissions:{userId}";
var permissions = await _cache.GetAsync<List<string>>(cacheKey);
if (permissions == null)
{
permissions = await LoadPermissionsFromDb(userId);
await _cache.SetAsync(cacheKey, permissions, TimeSpan.FromHours(2));
}
// 权限变更时清除缓存
await _cache.RemoveAsync($"user:permissions:{userId}");4. 前端权限指令
vue
<template>
<!-- 使用自定义指令 -->
<el-button v-permission="'user.edit'">编辑</el-button>
<el-button v-permission="'user.delete'">删除</el-button>
</template>
<script>
// 注册全局指令
app.directive('permission', {
mounted(el, binding) {
const { value } = binding
const permissions = store.userInfos.permissions
if (!permissions.includes(value)) {
el.parentNode?.removeChild(el)
}
}
})
</script>❓ 常见问题
Q: 如何添加新的权限?
A:
- 在数据库中添加到
fd_menu_button表 - 在角色的权限配置中勾选该权限
- 前端使用
v-permission指令控制显示 - 后端使用
[Authorize(Policy = "xxx")]控制访问
Q: 数据权限如何实现?
A:
- 实体实现
IHaveOwner接口(包含 CreatedUserId) - 创建对应的
IPermissionStrategy<T>实现 - 在查询时自动注入过滤条件
- 在更新/删除前调用
CanAccessAsync校验
Q: 权限变更后何时生效?
A:
- API 权限:下次请求时生效(Token 中包含最新权限)
- 菜单权限:重新登录或刷新页面后生效
- 按钮权限:前端重新获取用户信息后生效
- 数据权限:下次查询时生效
Q: 如何调试权限问题?
A:
- 检查 JWT Token 中的 Claims 是否包含正确权限
- 查看浏览器 Console 中的权限检查结果
- 检查后端日志中的授权失败信息
- 验证数据库中的权限配置是否正确
🔗 相关链接
- 架构设计 - 系统整体架构
- PluginA演示插件 - 权限扩展示例
- 后端开发 - API 权限控制
- 前端开发 - 前端权限实现