纸上得来终觉浅,绝知此事要躬行。嗨,大家好!我是码农刚子。在企业级应用开发中,权限管理是后台系统的核心基础设施之一。一个设计良好、易于维护的权限模型不仅能保障系统安全,还能提升开发效率。本文将基于 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)。UserRole和RolePermission为多对多关联表,支持用户多角色、角色多权限。- 可根据实际需要扩展字段,如软删除、租户隔离等。
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.SqlServerMicrosoft.AspNetCore.Authentication.JwtBearerSwashbuckle.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 #数据库设计 #后台开发