纸上得来终觉浅,绝知此事要躬行。嗨,大家好!我是码农刚子。在企业级应用开发中,权限管理是后台系统的核心基础设施之一。一个设计良好、易于维护的权限模型不仅能保障系统安全,还能提升开发效率。本文将基于 RBAC(基于角色的访问控制) 模型,结合 .NET Core / .NET Framework 后端、SQL Server 数据库以及 Vue 前端,为你提供一套开箱即用、可复用的权限系统设计方案。无论你是从零搭建新系统,还是为现有项目引入权限模块,本文的代码和思路都能帮助你快速落地。
一、RBAC 核心概念
RBAC 通过引入“角色”作为用户与权限的桥梁,将权限授予角色,再将角色授予用户,从而简化权限管理。核心元素包括:
- 用户(User):系统的操作者。
- 角色(Role):权限的集合,例如“管理员”、“财务专员”。
- 权限(Permission):对某个资源(如菜单、按钮、API)的操作许可。
- 用户-角色关联:用户拥有的角色。
- 角色-权限关联:角色拥有的权限。
此外,在实际企业系统中,我们通常还需要管理菜单(Menu)和按钮(Button),以实现前端动态路由和界面元素的权限控制。
二、数据库设计(SQL Server)
首先设计数据表,这是整个权限系统的基石。以下脚本采用 SQL Server,但稍作修改即可适配其他数据库。
2.1 核心表结构
- -- 用户表
- CREATE TABLE [User] (
- Id INT PRIMARY KEY IDENTITY(1,1),
- UserName NVARCHAR(50) NOT NULL UNIQUE,
- Password NVARCHAR(128) NOT NULL, -- 存储哈希后的密码
- RealName NVARCHAR(50),
- Email NVARCHAR(100),
- Phone NVARCHAR(20),
- IsEnabled BIT DEFAULT 1,
- CreatedAt DATETIME DEFAULT GETDATE()
- );
- -- 角色表
- CREATE TABLE [Role] (
- Id INT PRIMARY KEY IDENTITY(1,1),
- RoleName NVARCHAR(50) NOT NULL UNIQUE,
- Description NVARCHAR(200),
- IsEnabled BIT DEFAULT 1
- );
- -- 权限表
- CREATE TABLE [Permission] (
- Id INT PRIMARY KEY IDENTITY(1,1),
- ParentId INT NULL, -- 父权限,用于树形结构
- PermissionName NVARCHAR(50) NOT NULL,
- PermissionCode NVARCHAR(100) NOT NULL UNIQUE, -- 权限唯一标识,如 "user:add"
- PermissionType TINYINT DEFAULT 1, -- 1-菜单 2-按钮 3-API
- Url NVARCHAR(200), -- 菜单路径或API路径
- Icon NVARCHAR(50), -- 菜单图标
- SortOrder INT DEFAULT 0,
- FOREIGN KEY (ParentId) REFERENCES Permission(Id)
- );
- -- 用户-角色关联表
- CREATE TABLE [UserRole] (
- UserId INT NOT NULL,
- RoleId INT NOT NULL,
- PRIMARY KEY (UserId, RoleId),
- FOREIGN KEY (UserId) REFERENCES [User](Id) ON DELETE CASCADE,
- FOREIGN KEY (RoleId) REFERENCES [Role](Id) ON DELETE CASCADE
- );
- -- 角色-权限关联表
- CREATE TABLE [RolePermission] (
- RoleId INT NOT NULL,
- PermissionId INT NOT NULL,
- PRIMARY KEY (RoleId, PermissionId),
- FOREIGN KEY (RoleId) REFERENCES [Role](Id) ON DELETE CASCADE,
- FOREIGN KEY (PermissionId) REFERENCES [Permission](Id) ON DELETE CASCADE
- );
复制代码 说明:
- Permission 表采用父子关系,可构建菜单树。PermissionCode 是后端进行 API 权限判定的关键字段(如 user:view, order:export)。
- UserRole 和 RolePermission 为多对多关联表,支持用户多角色、角色多权限。
- 可根据实际需要扩展字段,如软删除、租户隔离等。
2.2 初始数据示例
- -- 插入权限
- INSERT INTO Permission (PermissionName, PermissionCode, PermissionType, Url, ParentId) VALUES
- ('系统管理', 'sys', 1, '/system', NULL),
- ('用户管理', 'sys:user', 1, '/system/user', 1),
- ('查看用户', 'sys:user:view', 2, NULL, 2),
- ('新增用户', 'sys:user:add', 2, NULL, 2),
- ('编辑用户', 'sys:user:edit', 2, NULL, 2),
- ('删除用户', 'sys:user:delete', 2, NULL, 2),
- ('角色管理', 'sys:role', 1, '/system/role', 1),
- ('查看角色', 'sys:role:view', 2, NULL, 7),
- ('分配权限', 'sys:role:assign', 2, NULL, 7);
- -- 插入角色
- INSERT INTO Role (RoleName, Description) VALUES ('超级管理员', '拥有所有权限'), ('普通用户', '仅有查看权限');
- -- 关联角色与权限(超级管理员拥有所有权限)
- INSERT INTO RolePermission (RoleId, PermissionId)
- SELECT 1, Id FROM Permission;
- -- 关联角色与权限(普通用户仅拥有查看权限)
- INSERT INTO RolePermission (RoleId, PermissionId)
- 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 方式,创建实体类:- public class User
- {
- public int Id { get; set; }
- public string UserName { get; set; }
- public string Password { get; set; } // 哈希值
- public string RealName { get; set; }
- public string Email { get; set; }
- public string Phone { get; set; }
- public bool IsEnabled { get; set; }
- public DateTime CreatedAt { get; set; }
- public ICollection<Role> Roles { get; set; }
- }
- public class Role
- {
- public int Id { get; set; }
- public string RoleName { get; set; }
- public string Description { get; set; }
- public bool IsEnabled { get; set; }
- public ICollection<User> Users { get; set; }
- public ICollection<Permission> Permissions { get; set; }
- }
- public class Permission
- {
- public int Id { get; set; }
- public int? ParentId { get; set; }
- public string PermissionName { get; set; }
- public string PermissionCode { get; set; }
- public byte PermissionType { get; set; } // 1-菜单 2-按钮 3-API
- public string Url { get; set; }
- public string Icon { get; set; }
- public int SortOrder { get; set; }
- public Permission Parent { get; set; }
- public ICollection<Permission> Children { get; set; }
- public ICollection<Role> Roles { get; set; }
- }
复制代码 配置多对多关系(在 DbContext.OnModelCreating 中):- modelBuilder.Entity<UserRole>()
- .HasKey(ur => new { ur.UserId, ur.RoleId });
- modelBuilder.Entity<RolePermission>()
- .HasKey(rp => new { rp.RoleId, rp.PermissionId });
复制代码 3.3 JWT 认证与登录接口
在 appsettings.json 中配置 JWT 参数:- "Jwt": {
- "Secret": "your-very-long-secret-key-here-change-it",
- "Issuer": "your-issuer",
- "Audience": "your-audience",
- "ExpireMinutes": 120
- }
复制代码 配置服务(Program.cs):- using Microsoft.AspNetCore.Authentication.JwtBearer;
- using Microsoft.IdentityModel.Tokens;
- using System.Text;
- // 添加 JWT 认证
- var key = Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Secret"]);
- builder.Services.AddAuthentication(x =>
- {
- x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
- x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
- })
- .AddJwtBearer(x =>
- {
- x.RequireHttpsMetadata = false;
- x.SaveToken = true;
- x.TokenValidationParameters = new TokenValidationParameters
- {
- ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(key),
- ValidateIssuer = true,
- ValidIssuer = builder.Configuration["Jwt:Issuer"],
- ValidateAudience = true,
- ValidAudience = builder.Configuration["Jwt:Audience"],
- ValidateLifetime = true,
- ClockSkew = TimeSpan.Zero
- };
- });
复制代码 登录接口(AuthController):- [HttpPost("login")]
- public async Task> Login(LoginDto loginDto)
- {
- var user = await _context.Users
- .Include(u => u.Roles)
- .ThenInclude(r => r.Permissions)
- .FirstOrDefaultAsync(u => u.UserName == loginDto.UserName && u.IsEnabled);
- if (user == null || !VerifyPassword(loginDto.Password, user.Password))
- return Unauthorized("用户名或密码错误");
- // 生成 JWT Token
- var claims = new List<Claim>
- {
- new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
- new Claim(ClaimTypes.Name, user.UserName),
- new Claim(ClaimTypes.GivenName, user.RealName ?? "")
- };
- // 将权限编码作为用户 Claims 的一部分(用于后端 API 权限判断)
- var permissions = user.Roles.SelectMany(r => r.Permissions)
- .Select(p => p.PermissionCode)
- .Distinct();
- foreach (var perm in permissions)
- {
- claims.Add(new Claim("Permission", perm));
- }
- var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]));
- var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
- var token = new JwtSecurityToken(
- issuer: _configuration["Jwt:Issuer"],
- audience: _configuration["Jwt:Audience"],
- claims: claims,
- expires: DateTime.Now.AddMinutes(double.Parse(_configuration["Jwt:ExpireMinutes"])),
- signingCredentials: creds
- );
- return Ok(new
- {
- token = new JwtSecurityTokenHandler().WriteToken(token),
- userInfo = new { user.Id, user.UserName, user.RealName }
- });
- }
复制代码 3.4 权限验证中间件与自定义特性
方案一:基于策略的授权
可以在 Program.cs 中定义策略,将权限编码映射到策略:- builder.Services.AddAuthorization(options =>
- {
- // 从数据库读取所有权限并添加策略(需在应用启动时执行一次)
- using var scope = builder.Services.BuildServiceProvider().CreateScope();
- var dbContext = scope.ServiceProvider.GetRequiredService();
- var permissions = dbContext.Permissions.Select(p => p.PermissionCode).ToList();
- foreach (var perm in permissions)
- {
- options.AddPolicy(perm, policy => policy.RequireClaim("Permission", perm));
- }
- });
复制代码 然后在 Controller 或 Action 上使用 [Authorize(Policy = "sys:user:view")]。
但这种方式在权限动态变化时需重启应用。更灵活的方式是使用自定义授权处理器。
方案二:自定义授权过滤器
创建一个自定义特性 PermissionAttribute,在 OnActionExecuting 中检查当前用户是否拥有指定权限编码。- [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
- public class PermissionAttribute : AuthorizeAttribute
- {
- public string Code { get; }
- public PermissionAttribute(string code)
- {
- Code = code;
- }
- }
- // 在过滤器管道中处理(需注册)
- public class PermissionFilter : IAsyncAuthorizationFilter
- {
- public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
- {
- var user = context.HttpContext.User;
- if (!user.Identity.IsAuthenticated)
- {
- context.Result = new UnauthorizedResult();
- return;
- }
- var permissionAttr = (context.ActionDescriptor as ControllerActionDescriptor)
- ?.MethodInfo.GetCustomAttribute<PermissionAttribute>();
- if (permissionAttr == null) return;
- var requiredCode = permissionAttr.Code;
- var userPermissions = user.FindAll("Permission").Select(c => c.Value).ToList();
- if (!userPermissions.Contains(requiredCode))
- {
- context.Result = new ForbidResult();
- }
- }
- }
- // 在 Program.cs 中注册
- builder.Services.AddControllers(options =>
- {
- options.Filters.Add<PermissionFilter>();
- });
复制代码 使用时:- [HttpGet]
- [Permission("sys:user:view")]
- public async Task<IActionResult> GetUsers()
- {
- // ...
- }
复制代码 这种方法权限变更后,用户需重新登录(或刷新 Token)才能生效。若需实时生效,可将权限存储在 Redis 中,并在过滤器中每次查询。
3.5 获取用户菜单与按钮权限接口
前端需要根据当前用户权限动态生成路由和按钮。后端提供两个接口:
- 获取菜单树:返回当前用户拥有的所有菜单类型权限(PermissionType=1)。
- 获取按钮权限码集合:返回当前用户拥有的所有按钮权限编码(PermissionType=2)。
- [HttpGet("menus")]
- [Permission("sys:menu")] // 可自定义权限
- public async Task>> GetUserMenus()
- {
- var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
- var user = await _context.Users
- .Include(u => u.Roles)
- .ThenInclude(r => r.Permissions)
- .FirstOrDefaultAsync(u => u.Id == userId);
- var menuPermissions = user.Roles
- .SelectMany(r => r.Permissions)
- .Where(p => p.PermissionType == 1) // 菜单
- .OrderBy(p => p.SortOrder)
- .ToList();
- // 构建树形结构
- var menuTree = BuildMenuTree(menuPermissions, null);
- return Ok(menuTree);
- }
- [HttpGet("buttons")]
- public async Task>> GetUserButtons()
- {
- var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
- var user = await _context.Users
- .Include(u => u.Roles)
- .ThenInclude(r => r.Permissions)
- .FirstOrDefaultAsync(u => u.Id == userId);
- var buttonCodes = user.Roles
- .SelectMany(r => r.Permissions)
- .Where(p => p.PermissionType == 2) // 按钮
- .Select(p => p.PermissionCode)
- .Distinct()
- .ToList();
- return Ok(buttonCodes);
- }
复制代码 四、前端集成(Vue 3 + Vue Router + Pinia)
前端使用 Vue 3 组合式 API,配合 Vue Router 和 Pinia 实现动态路由和按钮级权限控制。
4.1 存储用户信息及权限
在 Pinia store 中保存 token、用户信息、菜单列表和按钮权限集合。- // stores/user.js
- import { defineStore } from 'pinia'
- import { login as apiLogin, getUserMenus, getUserButtons } from '@/api/auth'
- export const useUserStore = defineStore('user', {
- state: () => ({
- token: localStorage.getItem('token') || '',
- userInfo: null,
- menus: [],
- buttons: []
- }),
- actions: {
- async login(credentials) {
- const res = await apiLogin(credentials)
- this.token = res.token
- localStorage.setItem('token', res.token)
- this.userInfo = res.userInfo
- await this.fetchPermissions()
- },
- async fetchPermissions() {
- const [menuRes, buttonRes] = await Promise.all([
- getUserMenus(),
- getUserButtons()
- ])
- this.menus = menuRes
- this.buttons = buttonRes
- },
- logout() {
- this.token = ''
- this.userInfo = null
- this.menus = []
- this.buttons = []
- localStorage.removeItem('token')
- }
- }
- })
复制代码 4.2 动态路由生成
在路由守卫中,根据后端返回的菜单树动态添加路由。- // router/index.js
- import { createRouter, createWebHistory } from 'vue-router'
- import { useUserStore } from '@/stores/user'
- // 静态路由(如登录页、404等)
- export const constantRoutes = [
- { path: '/login', component: () => import('@/views/Login.vue'), hidden: true },
- { path: '/404', component: () => import('@/views/404.vue'), hidden: true }
- ]
- // 异步路由(需动态添加)
- const asyncRoutes = [] // 初始为空
- const router = createRouter({
- history: createWebHistory(),
- routes: constantRoutes
- })
- // 重置路由(用于注销)
- export function resetRouter() {
- router.getRoutes().forEach(route => {
- if (route.name && !constantRoutes.some(r => r.name === route.name)) {
- router.removeRoute(route.name)
- }
- })
- }
- // 递归生成路由配置
- function generateRoutes(menus) {
- const routes = []
- menus.forEach(menu => {
- const route = {
- path: menu.url,
- name: menu.permissionCode, // 可选
- component: () => import(`@/views${menu.url}.vue`), // 根据路径映射组件
- meta: { title: menu.permissionName, icon: menu.icon },
- children: menu.children ? generateRoutes(menu.children) : []
- }
- routes.push(route)
- })
- return routes
- }
- // 路由守卫
- router.beforeEach(async (to, from, next) => {
- const userStore = useUserStore()
- if (to.path === '/login') {
- next()
- } else {
- if (!userStore.token) {
- next('/login')
- } else {
- // 如果菜单为空,说明刚登录或刷新页面,需要拉取权限并动态添加路由
- if (userStore.menus.length === 0) {
- await userStore.fetchPermissions()
- const routes = generateRoutes(userStore.menus)
- routes.forEach(route => router.addRoute(route))
- // 添加404重定向
- router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' })
- next(to.path) // 重新导航到目标
- } else {
- next()
- }
- }
- }
- })
复制代码 4.3 按钮级权限指令
封装一个自定义指令 v-permission,用于控制按钮的显示隐藏。- // directives/permission.js
- import { useUserStore } from '@/stores/user'
- export const permission = {
- mounted(el, binding) {
- const userStore = useUserStore()
- const { value } = binding
- if (value && !userStore.buttons.includes(value)) {
- el.parentNode?.removeChild(el)
- }
- }
- }
- // main.js 中注册
- import { permission } from '@/directives/permission'
- app.directive('permission', permission)
复制代码 使用示例:- <template>
- <button v-permission="'sys:user:add'">新增用户</button>
- </template>
复制代码 五、可复用组件与工具类
为了提升复用性,可以将权限相关逻辑封装成以下模块:
5.1 后端通用权限验证服务
- public interface IPermissionService
- {
- Task<bool> HasPermissionAsync(int userId, string permissionCode);
- Task<List<string>> GetUserPermissionsAsync(int userId);
- }
- public class PermissionService : IPermissionService
- {
- private readonly AppDbContext _dbContext;
- public PermissionService(AppDbContext dbContext) => _dbContext = dbContext;
- public async Task<bool> HasPermissionAsync(int userId, string permissionCode)
- {
- return await _dbContext.UserRoles
- .Where(ur => ur.UserId == userId)
- .SelectMany(ur => ur.Role.RolePermissions)
- .AnyAsync(rp => rp.Permission.PermissionCode == permissionCode);
- }
- public async Task<List<string>> GetUserPermissionsAsync(int userId)
- {
- return await _dbContext.UserRoles
- .Where(ur => ur.UserId == userId)
- .SelectMany(ur => ur.Role.RolePermissions)
- .Select(rp => rp.Permission.PermissionCode)
- .Distinct()
- .ToListAsync();
- }
- }
复制代码 可在需要的地方通过 DI 注入使用。
5.2 前端权限 Hook
封装一个组合式函数,方便在组件内判断权限:- // composables/usePermission.js
- import { useUserStore } from '@/stores/user'
- export function usePermission() {
- const userStore = useUserStore()
- function hasPermission(permissionCode) {
- return userStore.buttons.includes(permissionCode)
- }
- return { hasPermission }
- }
复制代码 使用:- <template>
- <button v-if="hasPermission('sys:user:add')">新增</button>
- </template>
复制代码 六、扩展考虑
6.1 数据权限
RBAC 模型通常只能控制“能否访问”,但实际业务中往往需要控制“能访问哪些数据”(如只能查看本部门数据)。此时可以引入“数据权限规则”,例如在权限表中增加 DataScope 字段,在查询时动态拼接过滤条件。
6.2 多租户
SaaS 系统中,需要在所有表增加 TenantId 字段,并在数据访问层自动过滤,实现租户隔离。
6.3 审计日志
记录用户操作日志,结合权限系统可追踪谁在何时做了什么。
七、总结
本文从数据库设计、后端实现到前端集成,完整地展示了一套基于 RBAC 的权限系统在 .NET + Vue 技术栈下的落地方法。你可以直接复制使用文中代码,并根据项目需求进行调整。关键要点:
- 数据库表结构设计清晰,权限编码规范化。
- 后端使用 JWT 携带权限 Claim,配合自定义过滤器实现 API 权限控制。
- 前端动态生成路由,利用指令实现按钮级权限。
- 封装可复用的服务和 Hook,方便业务层调用。
这套方案已在实际企业后台系统中多次验证,具有良好的扩展性和可维护性。希望对你有所帮助,欢迎在评论区交流讨论!
本人擅长 .NET 后台、小程序、商城定制开发,有需求可私信。
#.NET权限系统 #RBAC #数据库设计 #后台开发
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |