1. 项目概述:当RBAC遇上.NET Core与Vue3
最近在重构公司内部的管理系统,选择了ASP.NET Core 7 + Vue3的技术栈组合。这个权限管理系统采用经典的RBAC(基于角色的访问控制)模型,前端基于vue3-element-admin框架,后端则是纯正的.NET 7 WebAPI。整套系统从设计到落地耗时约两个月,期间踩过不少坑也积累了些实战经验,今天就把这个项目的完整实现路径分享给大家。
为什么选择这个技术组合?首先,.NET Core的性能表现有目共睹,特别是在高并发场景下的稳定性;其次,Vue3的Composition API让前端权限逻辑的组织更加清晰;最后,element-plus作为成熟的UI库,能大幅减少基础组件开发时间。三者结合既保证了系统性能,又提升了开发效率。
2. 核心架构设计
2.1 整体技术栈选型
后端技术矩阵:
- 框架:ASP.NET Core 7.0
- ORM:Entity Framework Core 7
- 认证:JWT + Cookies双验证
- 数据库:SQL Server 2019
- 缓存:Redis 6
- 部署:Docker + Nginx
前端技术矩阵:
- 框架:Vue3 + TypeScript
- UI库:Element Plus
- 状态管理:Pinia
- 路由:Vue Router 4
- 构建工具:Vite 4
提示:选择EF Core而非Dapper是考虑到本项目有复杂的权限关系模型,EF的导航属性可以简化关联查询。如果追求极致性能,可以考虑在热点接口改用Dapper。
2.2 数据库ER图设计
权限系统的核心五表结构:
-
用户表(Users)
sql复制CREATE TABLE [Users] ( [Id] BIGINT PRIMARY KEY, [UserName] NVARCHAR(50) NOT NULL, [PasswordHash] VARBINARY(MAX) NOT NULL, [Salt] VARBINARY(16) NOT NULL, [IsActive] BIT DEFAULT 1, [CreatedTime] DATETIME2 DEFAULT GETDATE() ) -
角色表(Roles)
sql复制CREATE TABLE [Roles] ( [Id] INT PRIMARY KEY, [RoleName] NVARCHAR(20) NOT NULL, [Description] NVARCHAR(100) ) -
权限表(Permissions)
sql复制CREATE TABLE [Permissions] ( [Id] UNIQUEIDENTIFIER PRIMARY KEY, [PermissionCode] VARCHAR(50) NOT NULL, [PermissionName] NVARCHAR(50) NOT NULL, [MenuIcon] VARCHAR(100), [RoutePath] VARCHAR(100) ) -
用户角色关联表(UserRoles)
sql复制CREATE TABLE [UserRoles] ( [UserId] BIGINT NOT NULL, [RoleId] INT NOT NULL, PRIMARY KEY ([UserId], [RoleId]) ) -
角色权限关联表(RolePermissions)
sql复制CREATE TABLE [RolePermissions] ( [RoleId] INT NOT NULL, [PermissionId] UNIQUEIDENTIFIER NOT NULL, PRIMARY KEY ([RoleId], [PermissionId]) )
这套设计的特点是:
- 采用雪花ID作为用户主键,避免自增ID暴露用户量
- 权限表包含路由路径信息,实现前后端路由统一管理
- 角色权限采用多对多关系,支持灵活的权限组合
3. 后端核心实现
3.1 JWT认证模块
在Program.cs中配置JWT:
csharp复制builder.Services.AddAuthentication(options => {
options.DefaultScheme = "JWT_OR_COOKIE";
options.DefaultChallengeScheme = "JWT_OR_COOKIE";
})
.AddPolicyScheme("JWT_OR_COOKIE", "JWT_OR_COOKIE", options => {
options.ForwardDefaultSelector = context => {
string authHeader = context.Request.Headers["Authorization"];
return !string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ")
? JwtBearerDefaults.AuthenticationScheme
: CookieAuthenticationDefaults.AuthenticationScheme;
};
})
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
ClockSkew = TimeSpan.Zero
};
})
.AddCookie();
注意:这里实现了JWT和Cookie的双重认证策略,前端可以根据运行环境选择认证方式。生产环境推荐使用JWT,开发环境可用Cookie简化调试。
3.2 权限检查中间件
创建PermissionMiddleware.cs:
csharp复制public class PermissionMiddleware {
private readonly RequestDelegate _next;
public PermissionMiddleware(RequestDelegate next) {
_next = next;
}
public async Task InvokeAsync(
HttpContext context,
IUserService userService) {
var endpoint = context.GetEndpoint();
var permissionAttr = endpoint?.Metadata
.GetMetadata<PermissionAttribute>();
if (permissionAttr != null) {
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (!await userService.CheckPermissionAsync(
long.Parse(userId),
permissionAttr.PermissionCode)) {
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Forbidden");
return;
}
}
await _next(context);
}
}
使用方式:在Controller方法上添加特性
csharp复制[HttpGet("users")]
[Permission("user:list")]
public async Task<IActionResult> GetUsers() {
// ...
}
4. 前端权限控制
4.1 动态路由生成
在router/index.ts中:
typescript复制const createRouter = async () => {
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/login',
component: () => import('@/views/login/index.vue')
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: await generateDynamicRoutes()
}
]
});
// 路由守卫
router.beforeEach(async (to) => {
const userStore = useUserStore();
if (!userStore.token && to.path !== '/login') {
return '/login';
}
if (to.meta.requiresAuth && !userStore.hasPermission(to.meta.permission)) {
return '/403';
}
});
return router;
};
const generateDynamicRoutes = async () => {
const { data } = await getCurrentUserPermissions();
return data.map(permission => ({
path: permission.routePath,
component: () => import(`@/views/${permission.componentPath}.vue`),
meta: {
title: permission.menuName,
icon: permission.menuIcon,
requiresAuth: true,
permission: permission.permissionCode
}
}));
};
4.2 按钮级权限控制
创建v-permission指令:
typescript复制const permission = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const userStore = useUserStore();
if (!userStore.hasPermission(binding.value)) {
el.parentNode?.removeChild(el);
}
}
};
// 使用方式
<el-button v-permission="'user:create'">新增用户</el-button>
5. 部署与性能优化
5.1 Docker部署配置
后端Dockerfile示例:
dockerfile复制FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish "PermissionSystem.WebAPI.csproj" -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /app
COPY --from=build /app .
ENV ASPNETCORE_URLS=http://+:5000
EXPOSE 5000
ENTRYPOINT ["dotnet", "PermissionSystem.WebAPI.dll"]
前端Dockerfile:
dockerfile复制FROM node:16 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
5.2 缓存策略优化
使用Redis缓存权限数据:
csharp复制// 在UserService中
public async Task<bool> CheckPermissionAsync(long userId, string permissionCode) {
var cacheKey = $"user:{userId}:permissions";
if (!_redisDatabase.KeyExists(cacheKey)) {
var permissions = await _dbContext.UserRoles
.Where(ur => ur.UserId == userId)
.SelectMany(ur => ur.Role.RolePermissions)
.Select(rp => rp.Permission.PermissionCode)
.ToListAsync();
_redisDatabase.StringSet(
cacheKey,
JsonSerializer.Serialize(permissions),
TimeSpan.FromMinutes(30));
}
var cachedPermissions = JsonSerializer.Deserialize<List<string>>(
_redisDatabase.StringGet(cacheKey));
return cachedPermissions.Contains(permissionCode);
}
6. 踩坑实录与解决方案
6.1 权限数据一致性问题
现象:修改角色权限后,部分用户仍能访问已取消权限的页面
解决方案:
csharp复制// 在RoleService中更新权限时
public async Task UpdateRolePermissions(int roleId, List<Guid> permissionIds) {
// 先更新数据库
await _dbContext.Database.BeginTransactionAsync();
try {
var existing = await _dbContext.RolePermissions
.Where(rp => rp.RoleId == roleId)
.ToListAsync();
_dbContext.RemoveRange(existing);
var newPermissions = permissionIds.Select(id => new RolePermission {
RoleId = roleId,
PermissionId = id
});
await _dbContext.AddRangeAsync(newPermissions);
await _dbContext.SaveChangesAsync();
await _dbContext.Database.CommitTransactionAsync();
// 清除相关缓存
var userIds = await _dbContext.UserRoles
.Where(ur => ur.RoleId == roleId)
.Select(ur => ur.UserId)
.ToListAsync();
var redisKeys = userIds.Select(id => $"user:{id}:permissions");
await _redisDatabase.KeyDeleteAsync(redisKeys.ToArray());
} catch {
await _dbContext.Database.RollbackTransactionAsync();
throw;
}
}
6.2 前端路由动态加载问题
现象:生产环境动态路由组件加载失败
解决方案:修改vite配置
javascript复制// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
}
}
}
})
并在路由生成时添加错误边界:
typescript复制const generateDynamicRoutes = async () => {
try {
const { data } = await getCurrentUserPermissions();
return await Promise.all(data.map(async permission => {
return {
path: permission.routePath,
component: () => import(`@/views/${permission.componentPath}.vue`)
.catch(() => import('@/views/error/404.vue')),
meta: {
title: permission.menuName,
icon: permission.menuIcon
}
};
}));
} catch {
return [];
}
};
7. 扩展功能实现
7.1 数据权限控制
在User实体中添加数据权限字段:
csharp复制public class User {
// ...其他字段
public DataPermissionLevel DataPermissionLevel { get; set; }
}
public enum DataPermissionLevel {
Self = 1, // 只能查看自己的数据
Department = 2, // 查看本部门数据
All = 3 // 查看所有数据
}
在Repository中应用过滤:
csharp复制public IQueryable<User> GetUserQueryable(long currentUserId, DataPermissionLevel level) {
var query = _dbContext.Users.AsQueryable();
if (level == DataPermissionLevel.Self) {
return query.Where(u => u.Id == currentUserId);
}
if (level == DataPermissionLevel.Department) {
var userDept = _dbContext.UserDepartments
.FirstOrDefault(ud => ud.UserId == currentUserId);
return userDept == null
? query.Where(u => false)
: query.Where(u => _dbContext.UserDepartments
.Any(ud => ud.UserId == u.Id && ud.DepartmentId == userDept.DepartmentId));
}
return query;
}
7.2 操作日志审计
创建审计日志中间件:
csharp复制public class AuditLogMiddleware {
private readonly RequestDelegate _next;
public AuditLogMiddleware(RequestDelegate next) {
_next = next;
}
public async Task InvokeAsync(HttpContext context, IAuditLogService logService) {
var endpoint = context.GetEndpoint();
if (endpoint?.Metadata.GetMetadata<NoAuditLogAttribute>() != null) {
await _next(context);
return;
}
var stopwatch = Stopwatch.StartNew();
var originalBodyStream = context.Response.Body;
try {
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
await _next(context);
stopwatch.Stop();
await logService.LogAsync(new AuditLog {
UserId = long.Parse(context.User.FindFirstValue(ClaimTypes.NameIdentifier)),
Action = $"{context.Request.Method} {context.Request.Path}",
Duration = stopwatch.ElapsedMilliseconds,
StatusCode = context.Response.StatusCode,
RequestTime = DateTime.UtcNow
});
await responseBody.CopyToAsync(originalBodyStream);
} finally {
context.Response.Body = originalBodyStream;
}
}
}
8. 项目优化建议
-
权限缓存预热:在系统启动时预加载常用角色的权限数据到Redis,减少首次请求延迟
-
批量权限检查:前端在初始化时一次性获取用户所有权限,避免频繁接口调用
-
权限变更通知:使用SignalR实现实时权限变更通知,强制前端重新获取权限数据
-
接口访问频控:对敏感权限接口添加速率限制,防止暴力破解
-
权限树形展示:后端返回权限的树形结构,方便前端展示嵌套菜单
实现权限树形结构的示例:
csharp复制public async Task<List<PermissionTree>> GetPermissionTreeAsync() {
var allPermissions = await _dbContext.Permissions.ToListAsync();
var rootPermissions = allPermissions
.Where(p => string.IsNullOrEmpty(p.ParentCode))
.OrderBy(p => p.Sort)
.ToList();
return rootPermissions.Select(p => BuildPermissionTree(p, allPermissions)).ToList();
}
private PermissionTree BuildPermissionTree(Permission permission, List<Permission> all) {
return new PermissionTree {
Id = permission.Id,
Name = permission.PermissionName,
Code = permission.PermissionCode,
Children = all.Where(p => p.ParentCode == permission.PermissionCode)
.OrderBy(p => p.Sort)
.Select(p => BuildPermissionTree(p, all))
.ToList()
};
}
这套系统目前已在生产环境稳定运行半年,支撑了公司200+员工的日常使用。最大的收获是认识到权限系统必须提前规划好扩展性,后期新增数据权限、字段权限等需求时,良好的架构设计可以避免大规模重构。