找回密码
 立即注册
首页 业界区 安全 .NET 权限系统(RBAC)怎么设计?直接可复用 ...

.NET 权限系统(RBAC)怎么设计?直接可复用

余思洁 昨天 11:50
纸上得来终觉浅,绝知此事要躬行。嗨,大家好!我是码农刚子。在企业级应用开发中,权限管理是后台系统的核心基础设施之一。一个设计良好、易于维护的权限模型不仅能保障系统安全,还能提升开发效率。本文将基于 RBAC(基于角色的访问控制) 模型,结合 .NET Core / .NET Framework 后端、SQL Server 数据库以及 Vue 前端,为你提供一套开箱即用、可复用的权限系统设计方案。无论你是从零搭建新系统,还是为现有项目引入权限模块,本文的代码和思路都能帮助你快速落地。
一、RBAC 核心概念

RBAC 通过引入“角色”作为用户与权限的桥梁,将权限授予角色,再将角色授予用户,从而简化权限管理。核心元素包括:

  • 用户(User):系统的操作者。
  • 角色(Role):权限的集合,例如“管理员”、“财务专员”。
  • 权限(Permission):对某个资源(如菜单、按钮、API)的操作许可。
  • 用户-角色关联:用户拥有的角色。
  • 角色-权限关联:角色拥有的权限。
此外,在实际企业系统中,我们通常还需要管理菜单(Menu)和按钮(Button),以实现前端动态路由和界面元素的权限控制。
二、数据库设计(SQL Server)

首先设计数据表,这是整个权限系统的基石。以下脚本采用 SQL Server,但稍作修改即可适配其他数据库。
2.1 核心表结构
  1. -- 用户表
  2. CREATE TABLE [User] (
  3.     Id          INT PRIMARY KEY IDENTITY(1,1),
  4.     UserName    NVARCHAR(50) NOT NULL UNIQUE,
  5.     Password    NVARCHAR(128) NOT NULL, -- 存储哈希后的密码
  6.     RealName    NVARCHAR(50),
  7.     Email       NVARCHAR(100),
  8.     Phone       NVARCHAR(20),
  9.     IsEnabled   BIT DEFAULT 1,
  10.     CreatedAt   DATETIME DEFAULT GETDATE()
  11. );
  12. -- 角色表
  13. CREATE TABLE [Role] (
  14.     Id          INT PRIMARY KEY IDENTITY(1,1),
  15.     RoleName    NVARCHAR(50) NOT NULL UNIQUE,
  16.     Description NVARCHAR(200),
  17.     IsEnabled   BIT DEFAULT 1
  18. );
  19. -- 权限表
  20. CREATE TABLE [Permission] (
  21.     Id          INT PRIMARY KEY IDENTITY(1,1),
  22.     ParentId    INT NULL,                      -- 父权限,用于树形结构
  23.     PermissionName NVARCHAR(50) NOT NULL,
  24.     PermissionCode NVARCHAR(100) NOT NULL UNIQUE, -- 权限唯一标识,如 "user:add"
  25.     PermissionType TINYINT DEFAULT 1,           -- 1-菜单 2-按钮 3-API
  26.     Url         NVARCHAR(200),                  -- 菜单路径或API路径
  27.     Icon        NVARCHAR(50),                    -- 菜单图标
  28.     SortOrder   INT DEFAULT 0,
  29.     FOREIGN KEY (ParentId) REFERENCES Permission(Id)
  30. );
  31. -- 用户-角色关联表
  32. CREATE TABLE [UserRole] (
  33.     UserId INT NOT NULL,
  34.     RoleId INT NOT NULL,
  35.     PRIMARY KEY (UserId, RoleId),
  36.     FOREIGN KEY (UserId) REFERENCES [User](Id) ON DELETE CASCADE,
  37.     FOREIGN KEY (RoleId) REFERENCES [Role](Id) ON DELETE CASCADE
  38. );
  39. -- 角色-权限关联表
  40. CREATE TABLE [RolePermission] (
  41.     RoleId       INT NOT NULL,
  42.     PermissionId INT NOT NULL,
  43.     PRIMARY KEY (RoleId, PermissionId),
  44.     FOREIGN KEY (RoleId) REFERENCES [Role](Id) ON DELETE CASCADE,
  45.     FOREIGN KEY (PermissionId) REFERENCES [Permission](Id) ON DELETE CASCADE
  46. );
复制代码
说明:

  • Permission 表采用父子关系,可构建菜单树。PermissionCode 是后端进行 API 权限判定的关键字段(如 user:view, order:export)。
  • UserRole 和 RolePermission 为多对多关联表,支持用户多角色、角色多权限。
  • 可根据实际需要扩展字段,如软删除、租户隔离等。
2.2 初始数据示例
  1. -- 插入权限
  2. INSERT INTO Permission (PermissionName, PermissionCode, PermissionType, Url, ParentId) VALUES
  3. ('系统管理', 'sys', 1, '/system', NULL),
  4. ('用户管理', 'sys:user', 1, '/system/user', 1),
  5. ('查看用户', 'sys:user:view', 2, NULL, 2),
  6. ('新增用户', 'sys:user:add', 2, NULL, 2),
  7. ('编辑用户', 'sys:user:edit', 2, NULL, 2),
  8. ('删除用户', 'sys:user:delete', 2, NULL, 2),
  9. ('角色管理', 'sys:role', 1, '/system/role', 1),
  10. ('查看角色', 'sys:role:view', 2, NULL, 7),
  11. ('分配权限', 'sys:role:assign', 2, NULL, 7);
  12. -- 插入角色
  13. INSERT INTO Role (RoleName, Description) VALUES ('超级管理员', '拥有所有权限'), ('普通用户', '仅有查看权限');
  14. -- 关联角色与权限(超级管理员拥有所有权限)
  15. INSERT INTO RolePermission (RoleId, PermissionId)
  16. SELECT 1, Id FROM Permission;
  17. -- 关联角色与权限(普通用户仅拥有查看权限)
  18. INSERT INTO RolePermission (RoleId, PermissionId)
  19. SELECT 2, Id FROM Permission WHERE PermissionCode LIKE '%view%';
复制代码
三、后端实现(.NET Core 6+)

后端基于 .NET Core 6(或更高版本)和 Entity Framework Core 构建,提供 JWT 认证、权限验证接口以及中间件。
3.1 项目结构与依赖

假设项目使用以下 NuGet 包:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.AspNetCore.Authentication.JwtBearer
  • Swashbuckle.AspNetCore(可选,用于 API 文档)
3.2 实体类映射

使用 EF Core Code First 方式,创建实体类:
  1. public class User
  2. {
  3.     public int Id { get; set; }
  4.     public string UserName { get; set; }
  5.     public string Password { get; set; } // 哈希值
  6.     public string RealName { get; set; }
  7.     public string Email { get; set; }
  8.     public string Phone { get; set; }
  9.     public bool IsEnabled { get; set; }
  10.     public DateTime CreatedAt { get; set; }
  11.     public ICollection<Role> Roles { get; set; }
  12. }
  13. public class Role
  14. {
  15.     public int Id { get; set; }
  16.     public string RoleName { get; set; }
  17.     public string Description { get; set; }
  18.     public bool IsEnabled { get; set; }
  19.     public ICollection<User> Users { get; set; }
  20.     public ICollection<Permission> Permissions { get; set; }
  21. }
  22. public class Permission
  23. {
  24.     public int Id { get; set; }
  25.     public int? ParentId { get; set; }
  26.     public string PermissionName { get; set; }
  27.     public string PermissionCode { get; set; }
  28.     public byte PermissionType { get; set; } // 1-菜单 2-按钮 3-API
  29.     public string Url { get; set; }
  30.     public string Icon { get; set; }
  31.     public int SortOrder { get; set; }
  32.     public Permission Parent { get; set; }
  33.     public ICollection<Permission> Children { get; set; }
  34.     public ICollection<Role> Roles { get; set; }
  35. }
复制代码
配置多对多关系(在 DbContext.OnModelCreating 中):
  1. modelBuilder.Entity<UserRole>()
  2.     .HasKey(ur => new { ur.UserId, ur.RoleId });
  3. modelBuilder.Entity<RolePermission>()
  4.     .HasKey(rp => new { rp.RoleId, rp.PermissionId });
复制代码
3.3 JWT 认证与登录接口

在 appsettings.json 中配置 JWT 参数:
  1. "Jwt": {
  2.   "Secret": "your-very-long-secret-key-here-change-it",
  3.   "Issuer": "your-issuer",
  4.   "Audience": "your-audience",
  5.   "ExpireMinutes": 120
  6. }
复制代码
配置服务(Program.cs):
  1. using Microsoft.AspNetCore.Authentication.JwtBearer;
  2. using Microsoft.IdentityModel.Tokens;
  3. using System.Text;
  4. // 添加 JWT 认证
  5. var key = Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Secret"]);
  6. builder.Services.AddAuthentication(x =>
  7. {
  8.     x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
  9.     x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
  10. })
  11. .AddJwtBearer(x =>
  12. {
  13.     x.RequireHttpsMetadata = false;
  14.     x.SaveToken = true;
  15.     x.TokenValidationParameters = new TokenValidationParameters
  16.     {
  17.         ValidateIssuerSigningKey = true,
  18.         IssuerSigningKey = new SymmetricSecurityKey(key),
  19.         ValidateIssuer = true,
  20.         ValidIssuer = builder.Configuration["Jwt:Issuer"],
  21.         ValidateAudience = true,
  22.         ValidAudience = builder.Configuration["Jwt:Audience"],
  23.         ValidateLifetime = true,
  24.         ClockSkew = TimeSpan.Zero
  25.     };
  26. });
复制代码
登录接口(AuthController):
  1. [HttpPost("login")]
  2. public async Task> Login(LoginDto loginDto)
  3. {
  4.     var user = await _context.Users
  5.         .Include(u => u.Roles)
  6.         .ThenInclude(r => r.Permissions)
  7.         .FirstOrDefaultAsync(u => u.UserName == loginDto.UserName && u.IsEnabled);
  8.     if (user == null || !VerifyPassword(loginDto.Password, user.Password))
  9.         return Unauthorized("用户名或密码错误");
  10.     // 生成 JWT Token
  11.     var claims = new List<Claim>
  12.     {
  13.         new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
  14.         new Claim(ClaimTypes.Name, user.UserName),
  15.         new Claim(ClaimTypes.GivenName, user.RealName ?? "")
  16.     };
  17.     // 将权限编码作为用户 Claims 的一部分(用于后端 API 权限判断)
  18.     var permissions = user.Roles.SelectMany(r => r.Permissions)
  19.                                 .Select(p => p.PermissionCode)
  20.                                 .Distinct();
  21.     foreach (var perm in permissions)
  22.     {
  23.         claims.Add(new Claim("Permission", perm));
  24.     }
  25.     var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]));
  26.     var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
  27.     var token = new JwtSecurityToken(
  28.         issuer: _configuration["Jwt:Issuer"],
  29.         audience: _configuration["Jwt:Audience"],
  30.         claims: claims,
  31.         expires: DateTime.Now.AddMinutes(double.Parse(_configuration["Jwt:ExpireMinutes"])),
  32.         signingCredentials: creds
  33.     );
  34.     return Ok(new
  35.     {
  36.         token = new JwtSecurityTokenHandler().WriteToken(token),
  37.         userInfo = new { user.Id, user.UserName, user.RealName }
  38.     });
  39. }
复制代码
3.4 权限验证中间件与自定义特性

方案一:基于策略的授权

可以在 Program.cs 中定义策略,将权限编码映射到策略:
  1. builder.Services.AddAuthorization(options =>
  2. {
  3.     // 从数据库读取所有权限并添加策略(需在应用启动时执行一次)
  4.     using var scope = builder.Services.BuildServiceProvider().CreateScope();
  5.     var dbContext = scope.ServiceProvider.GetRequiredService();
  6.     var permissions = dbContext.Permissions.Select(p => p.PermissionCode).ToList();
  7.     foreach (var perm in permissions)
  8.     {
  9.         options.AddPolicy(perm, policy => policy.RequireClaim("Permission", perm));
  10.     }
  11. });
复制代码
然后在 Controller 或 Action 上使用 [Authorize(Policy = "sys:user:view")]。
但这种方式在权限动态变化时需重启应用。更灵活的方式是使用自定义授权处理器。
方案二:自定义授权过滤器

创建一个自定义特性 PermissionAttribute,在 OnActionExecuting 中检查当前用户是否拥有指定权限编码。
  1. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
  2. public class PermissionAttribute : AuthorizeAttribute
  3. {
  4.     public string Code { get; }
  5.     public PermissionAttribute(string code)
  6.     {
  7.         Code = code;
  8.     }
  9. }
  10. // 在过滤器管道中处理(需注册)
  11. public class PermissionFilter : IAsyncAuthorizationFilter
  12. {
  13.     public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
  14.     {
  15.         var user = context.HttpContext.User;
  16.         if (!user.Identity.IsAuthenticated)
  17.         {
  18.             context.Result = new UnauthorizedResult();
  19.             return;
  20.         }
  21.         var permissionAttr = (context.ActionDescriptor as ControllerActionDescriptor)
  22.             ?.MethodInfo.GetCustomAttribute<PermissionAttribute>();
  23.         if (permissionAttr == null) return;
  24.         var requiredCode = permissionAttr.Code;
  25.         var userPermissions = user.FindAll("Permission").Select(c => c.Value).ToList();
  26.         if (!userPermissions.Contains(requiredCode))
  27.         {
  28.             context.Result = new ForbidResult();
  29.         }
  30.     }
  31. }
  32. // 在 Program.cs 中注册
  33. builder.Services.AddControllers(options =>
  34. {
  35.     options.Filters.Add<PermissionFilter>();
  36. });
复制代码
使用时:
  1. [HttpGet]
  2. [Permission("sys:user:view")]
  3. public async Task<IActionResult> GetUsers()
  4. {
  5.     // ...
  6. }
复制代码
这种方法权限变更后,用户需重新登录(或刷新 Token)才能生效。若需实时生效,可将权限存储在 Redis 中,并在过滤器中每次查询。
3.5 获取用户菜单与按钮权限接口

前端需要根据当前用户权限动态生成路由和按钮。后端提供两个接口:

  • 获取菜单树:返回当前用户拥有的所有菜单类型权限(PermissionType=1)。
  • 获取按钮权限码集合:返回当前用户拥有的所有按钮权限编码(PermissionType=2)。
  1. [HttpGet("menus")]
  2. [Permission("sys:menu")] // 可自定义权限
  3. public async Task>> GetUserMenus()
  4. {
  5.     var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
  6.     var user = await _context.Users
  7.         .Include(u => u.Roles)
  8.         .ThenInclude(r => r.Permissions)
  9.         .FirstOrDefaultAsync(u => u.Id == userId);
  10.     var menuPermissions = user.Roles
  11.         .SelectMany(r => r.Permissions)
  12.         .Where(p => p.PermissionType == 1) // 菜单
  13.         .OrderBy(p => p.SortOrder)
  14.         .ToList();
  15.     // 构建树形结构
  16.     var menuTree = BuildMenuTree(menuPermissions, null);
  17.     return Ok(menuTree);
  18. }
  19. [HttpGet("buttons")]
  20. public async Task>> GetUserButtons()
  21. {
  22.     var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
  23.     var user = await _context.Users
  24.         .Include(u => u.Roles)
  25.         .ThenInclude(r => r.Permissions)
  26.         .FirstOrDefaultAsync(u => u.Id == userId);
  27.     var buttonCodes = user.Roles
  28.         .SelectMany(r => r.Permissions)
  29.         .Where(p => p.PermissionType == 2) // 按钮
  30.         .Select(p => p.PermissionCode)
  31.         .Distinct()
  32.         .ToList();
  33.     return Ok(buttonCodes);
  34. }
复制代码
四、前端集成(Vue 3 + Vue Router + Pinia)

前端使用 Vue 3 组合式 API,配合 Vue Router 和 Pinia 实现动态路由和按钮级权限控制。
4.1 存储用户信息及权限

在 Pinia store 中保存 token、用户信息、菜单列表和按钮权限集合。
  1. // stores/user.js
  2. import { defineStore } from 'pinia'
  3. import { login as apiLogin, getUserMenus, getUserButtons } from '@/api/auth'
  4. export const useUserStore = defineStore('user', {
  5.   state: () => ({
  6.     token: localStorage.getItem('token') || '',
  7.     userInfo: null,
  8.     menus: [],
  9.     buttons: []
  10.   }),
  11.   actions: {
  12.     async login(credentials) {
  13.       const res = await apiLogin(credentials)
  14.       this.token = res.token
  15.       localStorage.setItem('token', res.token)
  16.       this.userInfo = res.userInfo
  17.       await this.fetchPermissions()
  18.     },
  19.     async fetchPermissions() {
  20.       const [menuRes, buttonRes] = await Promise.all([
  21.         getUserMenus(),
  22.         getUserButtons()
  23.       ])
  24.       this.menus = menuRes
  25.       this.buttons = buttonRes
  26.     },
  27.     logout() {
  28.       this.token = ''
  29.       this.userInfo = null
  30.       this.menus = []
  31.       this.buttons = []
  32.       localStorage.removeItem('token')
  33.     }
  34.   }
  35. })
复制代码
4.2 动态路由生成

在路由守卫中,根据后端返回的菜单树动态添加路由。
  1. // router/index.js
  2. import { createRouter, createWebHistory } from 'vue-router'
  3. import { useUserStore } from '@/stores/user'
  4. // 静态路由(如登录页、404等)
  5. export const constantRoutes = [
  6.   { path: '/login', component: () => import('@/views/Login.vue'), hidden: true },
  7.   { path: '/404', component: () => import('@/views/404.vue'), hidden: true }
  8. ]
  9. // 异步路由(需动态添加)
  10. const asyncRoutes = [] // 初始为空
  11. const router = createRouter({
  12.   history: createWebHistory(),
  13.   routes: constantRoutes
  14. })
  15. // 重置路由(用于注销)
  16. export function resetRouter() {
  17.   router.getRoutes().forEach(route => {
  18.     if (route.name && !constantRoutes.some(r => r.name === route.name)) {
  19.       router.removeRoute(route.name)
  20.     }
  21.   })
  22. }
  23. // 递归生成路由配置
  24. function generateRoutes(menus) {
  25.   const routes = []
  26.   menus.forEach(menu => {
  27.     const route = {
  28.       path: menu.url,
  29.       name: menu.permissionCode, // 可选
  30.       component: () => import(`@/views${menu.url}.vue`), // 根据路径映射组件
  31.       meta: { title: menu.permissionName, icon: menu.icon },
  32.       children: menu.children ? generateRoutes(menu.children) : []
  33.     }
  34.     routes.push(route)
  35.   })
  36.   return routes
  37. }
  38. // 路由守卫
  39. router.beforeEach(async (to, from, next) => {
  40.   const userStore = useUserStore()
  41.   if (to.path === '/login') {
  42.     next()
  43.   } else {
  44.     if (!userStore.token) {
  45.       next('/login')
  46.     } else {
  47.       // 如果菜单为空,说明刚登录或刷新页面,需要拉取权限并动态添加路由
  48.       if (userStore.menus.length === 0) {
  49.         await userStore.fetchPermissions()
  50.         const routes = generateRoutes(userStore.menus)
  51.         routes.forEach(route => router.addRoute(route))
  52.         // 添加404重定向
  53.         router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' })
  54.         next(to.path) // 重新导航到目标
  55.       } else {
  56.         next()
  57.       }
  58.     }
  59.   }
  60. })
复制代码
4.3 按钮级权限指令

封装一个自定义指令 v-permission,用于控制按钮的显示隐藏。
  1. // directives/permission.js
  2. import { useUserStore } from '@/stores/user'
  3. export const permission = {
  4.   mounted(el, binding) {
  5.     const userStore = useUserStore()
  6.     const { value } = binding
  7.     if (value && !userStore.buttons.includes(value)) {
  8.       el.parentNode?.removeChild(el)
  9.     }
  10.   }
  11. }
  12. // main.js 中注册
  13. import { permission } from '@/directives/permission'
  14. app.directive('permission', permission)
复制代码
使用示例:
  1. <template>
  2.   <button v-permission="'sys:user:add'">新增用户</button>
  3. </template>
复制代码
五、可复用组件与工具类

为了提升复用性,可以将权限相关逻辑封装成以下模块:
5.1 后端通用权限验证服务
  1. public interface IPermissionService
  2. {
  3.     Task<bool> HasPermissionAsync(int userId, string permissionCode);
  4.     Task<List<string>> GetUserPermissionsAsync(int userId);
  5. }
  6. public class PermissionService : IPermissionService
  7. {
  8.     private readonly AppDbContext _dbContext;
  9.     public PermissionService(AppDbContext dbContext) => _dbContext = dbContext;
  10.     public async Task<bool> HasPermissionAsync(int userId, string permissionCode)
  11.     {
  12.         return await _dbContext.UserRoles
  13.             .Where(ur => ur.UserId == userId)
  14.             .SelectMany(ur => ur.Role.RolePermissions)
  15.             .AnyAsync(rp => rp.Permission.PermissionCode == permissionCode);
  16.     }
  17.     public async Task<List<string>> GetUserPermissionsAsync(int userId)
  18.     {
  19.         return await _dbContext.UserRoles
  20.             .Where(ur => ur.UserId == userId)
  21.             .SelectMany(ur => ur.Role.RolePermissions)
  22.             .Select(rp => rp.Permission.PermissionCode)
  23.             .Distinct()
  24.             .ToListAsync();
  25.     }
  26. }
复制代码
可在需要的地方通过 DI 注入使用。
5.2 前端权限 Hook

封装一个组合式函数,方便在组件内判断权限:
  1. // composables/usePermission.js
  2. import { useUserStore } from '@/stores/user'
  3. export function usePermission() {
  4.   const userStore = useUserStore()
  5.   function hasPermission(permissionCode) {
  6.     return userStore.buttons.includes(permissionCode)
  7.   }
  8.   return { hasPermission }
  9. }
复制代码
使用:
  1. <template>
  2.   <button v-if="hasPermission('sys:user:add')">新增</button>
  3. </template>
复制代码
六、扩展考虑

6.1 数据权限

RBAC 模型通常只能控制“能否访问”,但实际业务中往往需要控制“能访问哪些数据”(如只能查看本部门数据)。此时可以引入“数据权限规则”,例如在权限表中增加 DataScope 字段,在查询时动态拼接过滤条件。
6.2 多租户

SaaS 系统中,需要在所有表增加 TenantId 字段,并在数据访问层自动过滤,实现租户隔离。
6.3 审计日志

记录用户操作日志,结合权限系统可追踪谁在何时做了什么。
七、总结

本文从数据库设计、后端实现到前端集成,完整地展示了一套基于 RBAC 的权限系统在 .NET + Vue 技术栈下的落地方法。你可以直接复制使用文中代码,并根据项目需求进行调整。关键要点:

  • 数据库表结构设计清晰,权限编码规范化。
  • 后端使用 JWT 携带权限 Claim,配合自定义过滤器实现 API 权限控制。
  • 前端动态生成路由,利用指令实现按钮级权限。
  • 封装可复用的服务和 Hook,方便业务层调用。
这套方案已在实际企业后台系统中多次验证,具有良好的扩展性和可维护性。希望对你有所帮助,欢迎在评论区交流讨论!
本人擅长 .NET 后台、小程序、商城定制开发,有需求可私信。
#.NET权限系统 #RBAC #数据库设计 #后台开发

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册