Skip to content

权限模型

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:

  1. 在数据库中添加到 fd_menu_button
  2. 在角色的权限配置中勾选该权限
  3. 前端使用 v-permission 指令控制显示
  4. 后端使用 [Authorize(Policy = "xxx")] 控制访问

Q: 数据权限如何实现?

A:

  1. 实体实现 IHaveOwner 接口(包含 CreatedUserId)
  2. 创建对应的 IPermissionStrategy<T> 实现
  3. 在查询时自动注入过滤条件
  4. 在更新/删除前调用 CanAccessAsync 校验

Q: 权限变更后何时生效?

A:

  • API 权限:下次请求时生效(Token 中包含最新权限)
  • 菜单权限:重新登录或刷新页面后生效
  • 按钮权限:前端重新获取用户信息后生效
  • 数据权限:下次查询时生效

Q: 如何调试权限问题?

A:

  1. 检查 JWT Token 中的 Claims 是否包含正确权限
  2. 查看浏览器 Console 中的权限检查结果
  3. 检查后端日志中的授权失败信息
  4. 验证数据库中的权限配置是否正确

🔗 相关链接

Released under the MIT License.