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

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

一、RBAC 核心概念

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

  • 用户(User):系统的操作者。
  • 角色(Role):权限的集合,例如"管理员"、"财务专员"。
  • 权限(Permission):对某个资源(如菜单、按钮、API)的操作许可。
  • 用户-角色关联:用户拥有的角色。
  • 角色-权限关联:角色拥有的权限。

此外,在实际企业系统中,我们通常还需要管理菜单(Menu)和按钮(Button),以实现前端动态路由和界面元素的权限控制。

二、数据库设计(SQL Server)

首先设计数据表,这是整个权限系统的基石。以下脚本采用 SQL Server,但稍作修改即可适配其他数据库。

2.1 核心表结构

sql 复制代码
-- 用户表
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)。
  • UserRoleRolePermission 为多对多关联表,支持用户多角色、角色多权限。
  • 可根据实际需要扩展字段,如软删除、租户隔离等。

2.2 初始数据示例

sql 复制代码
-- 插入权限
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 方式,创建实体类:

csharp 复制代码
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 中):

ini 复制代码
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 参数:

json 复制代码
"Jwt": {
  "Secret": "your-very-long-secret-key-here-change-it",
  "Issuer": "your-issuer",
  "Audience": "your-audience",
  "ExpireMinutes": 120
}

配置服务(Program.cs):

ini 复制代码
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):

ini 复制代码
[HttpPost("login")]
public async Task<ActionResult<LoginResult>> 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 中定义策略,将权限编码映射到策略:

ini 复制代码
builder.Services.AddAuthorization(options =>
{
    // 从数据库读取所有权限并添加策略(需在应用启动时执行一次)
    using var scope = builder.Services.BuildServiceProvider().CreateScope();
    var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    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 中检查当前用户是否拥有指定权限编码。

ini 复制代码
[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>();
});

使用时:

csharp 复制代码
[HttpGet]
[Permission("sys:user:view")]
public async Task<IActionResult> GetUsers()
{
    // ...
}

这种方法权限变更后,用户需重新登录(或刷新 Token)才能生效。若需实时生效,可将权限存储在 Redis 中,并在过滤器中每次查询。

3.5 获取用户菜单与按钮权限接口

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

  • 获取菜单树:返回当前用户拥有的所有菜单类型权限(PermissionType=1)。
  • 获取按钮权限码集合:返回当前用户拥有的所有按钮权限编码(PermissionType=2)。
ini 复制代码
[HttpGet("menus")]
[Permission("sys:menu")] // 可自定义权限
public async Task<ActionResult<List<MenuDto>>> 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<ActionResult<List<string>>> 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、用户信息、菜单列表和按钮权限集合。

javascript 复制代码
// 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 动态路由生成

在路由守卫中,根据后端返回的菜单树动态添加路由。

javascript 复制代码
// 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,用于控制按钮的显示隐藏。

javascript 复制代码
// 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)

使用示例:

xml 复制代码
<template>
  <button v-permission="'sys:user:add'">新增用户</button>
</template>

五、可复用组件与工具类

为了提升复用性,可以将权限相关逻辑封装成以下模块:

5.1 后端通用权限验证服务

csharp 复制代码
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

封装一个组合式函数,方便在组件内判断权限:

javascript 复制代码
// composables/usePermission.js
import { useUserStore } from '@/stores/user'

export function usePermission() {
  const userStore = useUserStore()

  function hasPermission(permissionCode) {
    return userStore.buttons.includes(permissionCode)
  }

  return { hasPermission }
}

使用:

xml 复制代码
<script setup>
import { usePermission } from '@/composables/usePermission'
const { hasPermission } = usePermission()
</script>

<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 #数据库设计 #后台开发

相关推荐
把你毕设抢过来2 小时前
基于Spring Boot的演唱会购票系统的设计与实现(源码+文档)
java·spring boot·后端
yiyaozjk2 小时前
Go基础之环境搭建
开发语言·后端·golang
颜酱2 小时前
环检测与拓扑排序:BFS/DFS双实现
javascript·后端·算法
code_YuJun2 小时前
数据库事务
后端
代码探秘者2 小时前
【Java集合】ArrayList :底层原理、数组互转与扩容计算
java·开发语言·jvm·数据库·后端·python·算法
泰式大师2 小时前
别再让 Agent 靠感觉改计划了:我把 Replan 做成了一个可计数的系统事件
后端
颜酱2 小时前
理解并查集Union-Find:从原理到练习
javascript·后端·算法
隔壁小邓2 小时前
分布式事务
java·后端
我叫黑大帅2 小时前
如何让两个Go程序远程调用?
后端·面试·go