无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(9) 角色管理,分配权限
在前面的章节中,我们已经完成了ASP.NET MVC项目的基础框架搭建、用户登录模块的实现。作为权限管理系统的核心环节,角色管理与权限分配是实现系统资源访问控制的关键——通过角色将权限批量赋予用户,既能简化权限管控流程,又能保障系统的安全性。本章将详细讲解角色的CRUD操作、权限分配的核心逻辑,以及如何在MVC中实现权限验证拦截,帮助你构建完整的权限体系。
目录#
- 前置回顾与本章目标
- 角色与权限的数据库设计优化
- 角色管理模块的功能实现 3.1 角色列表展示(Index) 3.2 角色新增/编辑(Create/Edit) 3.3 角色删除(Delete)
- 权限分配核心实现 4.1 权限数据的加载与分组展示 4.2 权限分配的逻辑处理 4.3 权限持久化到数据库
- ASP.NET MVC中的权限验证拦截 5.1 自定义AuthorizeAttribute 5.2 控制器/Action级别的权限标记
- 最佳实践与常见问题
- 本章总结与下章预告
- 参考资料
1. 前置回顾与本章目标#
前置回顾#
- 已完成ASP.NET MVC项目基础框架(路由、EF上下文、布局页)
- 实现了用户登录、身份认证功能
- 数据库已有用户表(ApplicationUser)
本章目标#
- 设计并实现角色、权限及关联关系的数据库模型
- 完成角色的增删改查(CRUD)功能
- 实现角色与权限的分配逻辑
- 自定义权限拦截器,实现控制器/Action级别的权限验证
2. 角色与权限的数据库设计优化#
一个完善的权限体系需要以下4张核心表,我们采用EF Code-First方式实现:
2.1 核心实体模型#
// 角色表
public class Role
{
public int RoleId { get; set; }
[Required(ErrorMessage = "角色名称不能为空")]
[MaxLength(50, ErrorMessage = "角色名称不能超过50字符")]
public string RoleName { get; set; }
[MaxLength(200)]
public string Description { get; set; }
public bool IsEnabled { get; set; } = true; // 是否启用角色
// 导航属性
public virtual ICollection<RolePermission> RolePermissions { get; set; }
public virtual ICollection<UserRole> UserRoles { get; set; }
}
// 权限表
public class Permission
{
public int PermissionId { get; set; }
[Required]
[MaxLength(50)]
public string PermissionName { get; set; } // 如"用户管理-查看"
[Required]
[MaxLength(50)]
public string PermissionCode { get; set; } // 如"User_View"(权限唯一标识)
[MaxLength(50)]
public string ModuleName { get; set; } // 所属模块,如"用户管理"
public int SortOrder { get; set; } // 排序优先级
}
// 角色-权限关联表(复合主键)
public class RolePermission
{
public int RoleId { get; set; }
public int PermissionId { get; set; }
// 导航属性
public virtual Role Role { get; set; }
public virtual Permission Permission { get; set; }
}
// 用户-角色关联表(复合主键)
public class UserRole
{
public string UserId { get; set; } // 关联AspNetUsers表的Id
public int RoleId { get; set; }
// 导航属性
public virtual ApplicationUser User { get; set; }
public virtual Role Role { get; set; }
}2.2 EF上下文配置#
修改ApplicationDbContext,添加实体集并配置关联关系:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public DbSet<Role> Roles { get; set; }
public DbSet<Permission> Permissions { get; set; }
public DbSet<RolePermission> RolePermissions { get; set; }
public DbSet<UserRole> UserRoles { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 配置角色-权限关联表的复合主键
modelBuilder.Entity<RolePermission>()
.HasKey(rp => new { rp.RoleId, rp.PermissionId });
// 配置用户-角色关联表的复合主键
modelBuilder.Entity<UserRole>()
.HasKey(ur => new { ur.UserId, ur.RoleId });
}
}2.3 初始化权限数据#
在项目启动时,可通过数据库初始化器预设基础权限(如用户管理、角色管理的常用权限):
public class PermissionInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
{
protected override void Seed(ApplicationDbContext context)
{
var permissions = new List<Permission>
{
new Permission { PermissionName="用户管理-查看", PermissionCode="User_View", ModuleName="用户管理", SortOrder=1 },
new Permission { PermissionName="用户管理-新增", PermissionCode="User_Create", ModuleName="用户管理", SortOrder=2 },
new Permission { PermissionName="角色管理-查看", PermissionCode="Role_View", ModuleName="角色管理", SortOrder=1 },
new Permission { PermissionName="角色管理-分配权限", PermissionCode="Role_AssignPerm", ModuleName="角色管理", SortOrder=3 }
};
context.Permissions.AddRange(permissions);
context.SaveChanges();
}
}3. 角色管理模块的功能实现#
3.1 角色列表展示(Index)#
创建RoleController,实现角色列表的查询与视图展示:
public class RoleController : Controller
{
private ApplicationDbContext db = new ApplicationDbContext();
// GET: Role
[CustomAuthorize("Role_View")] // 后续将实现自定义权限标记
public ActionResult Index()
{
var roles = db.Roles.Where(r => r.IsEnabled).ToList();
return View(roles);
}
}对应的Index.cshtml视图:
@model IEnumerable<YourProject.Models.Role>
<h2>角色管理</h2>
<p>
@Html.ActionLink("新增角色", "Create", null, new { @class = "btn btn-primary" })
</p>
<table class="table table-striped">
<tr>
<th>角色名称</th>
<th>描述</th>
<th>操作</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>@Html.DisplayFor(modelItem => item.RoleName)</td>
<td>@Html.DisplayFor(modelItem => item.Description)</td>
<td>
@Html.ActionLink("编辑", "Edit", new { id=item.RoleId }) |
@Html.ActionLink("分配权限", "AssignPermissions", new { roleId=item.RoleId }) |
@Html.ActionLink("删除", "Delete", new { id=item.RoleId })
</td>
</tr>
}
</table>3.2 角色新增/编辑(Create/Edit)#
新增角色的Action与视图:
// GET: Role/Create
public ActionResult Create()
{
return View();
}
// POST: Role/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "RoleName,Description")] Role role)
{
if (ModelState.IsValid)
{
role.IsEnabled = true;
db.Roles.Add(role);
db.SaveChanges();
TempData["SuccessMsg"] = "角色创建成功!";
return RedirectToAction("Index");
}
return View(role);
}Create.cshtml视图(表单+数据验证):
@model YourProject.Models.Role
<h2>新增角色</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.RoleName, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.RoleName, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.RoleName, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextAreaFor(model => model.Description, new { @class = "form-control", rows = 3 })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="保存" class="btn btn-primary" />
@Html.ActionLink("返回列表", "Index", null, new { @class = "btn btn-default" })
</div>
</div>
</div>
}编辑逻辑与新增类似,仅需根据RoleId查询现有角色数据并回显到表单。
3.3 角色删除(Delete)#
实现软删除(标记IsEnabled为false)而非硬删除,避免数据丢失:
// GET: Role/Delete/5
public ActionResult Delete(int? id)
{
if (id == null) return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
Role role = db.Roles.Find(id);
if (role == null) return HttpNotFound();
return View(role);
}
// POST: Role/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
Role role = db.Roles.Find(id);
role.IsEnabled = false; // 软删除
db.SaveChanges();
TempData["SuccessMsg"] = "角色已禁用!";
return RedirectToAction("Index");
}4. 权限分配核心实现#
权限分配的核心是展示所有权限,并让用户勾选当前角色需要拥有的权限,最终将关联关系持久化到数据库。
4.1 设计权限分配视图模型#
为了在视图中更清晰地展示权限,我们设计分层的视图模型:
// 权限项视图模型
public class PermissionItemViewModel
{
public int PermissionId { get; set; }
public string PermissionName { get; set; }
public string PermissionCode { get; set; }
public bool IsAssigned { get; set; } // 是否已分配给当前角色
}
// 权限组(按模块分组)视图模型
public class PermissionGroupViewModel
{
public string ModuleName { get; set; }
public List<PermissionItemViewModel> Permissions { get; set; }
}
// 角色权限分配视图模型
public class RolePermissionAssignViewModel
{
public int RoleId { get; set; }
public string RoleName { get; set; }
public List<PermissionGroupViewModel> PermissionGroups { get; set; }
public int[] SelectedPermissions { get; set; } // 提交时的选中权限ID数组
}4.2 权限分配的GET请求处理#
加载当前角色的已有权限,并按模块分组展示:
// GET: Role/AssignPermissions/5
public ActionResult AssignPermissions(int roleId)
{
var role = db.Roles.Find(roleId);
if (role == null) return HttpNotFound();
// 获取所有权限
var allPermissions = db.Permissions.OrderBy(p => p.SortOrder).ToList();
// 获取当前角色已分配的权限ID
var assignedPermIds = db.RolePermissions
.Where(rp => rp.RoleId == roleId)
.Select(rp => rp.PermissionId)
.ToList();
// 按模块分组权限
var permissionGroups = allPermissions
.GroupBy(p => p.ModuleName)
.Select(g => new PermissionGroupViewModel
{
ModuleName = g.Key,
Permissions = g.Select(p => new PermissionItemViewModel
{
PermissionId = p.PermissionId,
PermissionName = p.PermissionName,
PermissionCode = p.PermissionCode,
IsAssigned = assignedPermIds.Contains(p.PermissionId)
}).ToList()
}).ToList();
var model = new RolePermissionAssignViewModel
{
RoleId = roleId,
RoleName = role.RoleName,
PermissionGroups = permissionGroups
};
return View(model);
}4.3 权限分配的POST请求处理#
先清空角色原有权限,再批量插入新选中的权限:
// POST: Role/AssignPermissions
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AssignPermissions(RolePermissionAssignViewModel model)
{
if (ModelState.IsValid)
{
// 1. 清空角色原有所有权限
var existingPerms = db.RolePermissions.Where(rp => rp.RoleId == model.RoleId);
db.RolePermissions.RemoveRange(existingPerms);
// 2. 插入新选中的权限
if (model.SelectedPermissions != null && model.SelectedPermissions.Length > 0)
{
var newRolePerms = model.SelectedPermissions
.Distinct()
.Select(pid => new RolePermission
{
RoleId = model.RoleId,
PermissionId = pid
}).ToList();
db.RolePermissions.AddRange(newRolePerms);
}
db.SaveChanges();
// 3. 清除该角色所有用户的权限缓存(后续实现)
PermissionHelper.ClearUserPermissionCacheByRole(model.RoleId);
TempData["SuccessMsg"] = "权限分配成功!";
return RedirectToAction("Index");
}
// 验证失败,重新加载视图数据
return View(model);
}4.4 权限分配视图(AssignPermissions.cshtml)#
使用复选框列表展示分组后的权限:
@model YourProject.ViewModels.RolePermissionAssignViewModel
<h2>为角色 "@Model.RoleName" 分配权限</h2>
@using (Html.BeginForm())
{
@Html.HiddenFor(m => m.RoleId)
<div class="permission-container">
@foreach (var module in Model.PermissionGroups)
{
<div class="panel panel-default">
<div class="panel-heading">@module.ModuleName</div>
<div class="panel-body">
@foreach (var perm in module.Permissions)
{
<div class="checkbox col-md-3">
<label>
@Html.CheckBoxFor(m => m.SelectedPermissions,
new { value = perm.PermissionId, @checked = perm.IsAssigned })
@perm.PermissionName
</label>
</div>
}
</div>
</div>
}
</div>
<div class="form-group text-center">
<button type="submit" class="btn btn-primary">保存权限分配</button>
@Html.ActionLink("返回角色列表", "Index", null, new { @class = "btn btn-default" })
</div>
}5. ASP.NET MVC中的权限验证拦截#
完成权限分配后,需要在MVC中实现权限验证,确保用户只能访问其拥有权限的资源。
5.1 自定义AuthorizeAttribute#
继承AuthorizeAttribute,实现自定义权限验证逻辑:
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
// 需要验证的权限Code
public string PermissionCode { get; set; }
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
// 1. 检查用户是否已登录
if (!httpContext.User.Identity.IsAuthenticated)
return false;
// 2. 检查是否为超级管理员(预设权限Code为"SuperAdmin")
var userPermissions = PermissionHelper.GetCurrentUserPermissions(httpContext);
if (userPermissions.Contains("SuperAdmin"))
return true;
// 3. 检查是否拥有目标权限
return !string.IsNullOrEmpty(PermissionCode) && userPermissions.Contains(PermissionCode);
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// 区分AJAX请求与普通请求,返回不同结果
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new JsonResult
{
Data = new { Success = false, Message = "无权限执行此操作" },
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
else
{
filterContext.Result = new RedirectResult("/Home/NoPermission");
}
}
}5.2 权限辅助类PermissionHelper#
封装用户权限的获取与缓存逻辑:
public static class PermissionHelper
{
/// <summary>
/// 获取当前登录用户的权限列表(优先从Session缓存获取)
/// </summary>
public static List<string> GetCurrentUserPermissions(HttpContextBase httpContext)
{
var permissions = httpContext.Session["UserPermissions"] as List<string>;
if (permissions == null)
{
var userId = httpContext.User.Identity.GetUserId();
permissions = GetUserPermissionsFromDb(userId);
httpContext.Session["UserPermissions"] = permissions;
}
return permissions;
}
/// <summary>
/// 从数据库查询用户权限
/// </summary>
private static List<string> GetUserPermissionsFromDb(string userId)
{
using (var db = new ApplicationDbContext())
{
return db.UserRoles
.Where(ur => ur.UserId == userId)
.SelectMany(ur => ur.Role.RolePermissions)
.Select(rp => rp.Permission.PermissionCode)
.Distinct()
.ToList();
}
}
/// <summary>
/// 清除指定角色下所有用户的权限缓存
/// </summary>
public static void ClearUserPermissionCacheByRole(int roleId)
{
using (var db = new ApplicationDbContext())
{
var userIds = db.UserRoles.Where(ur => ur.RoleId == roleId).Select(ur => ur.UserId).ToList();
// 遍历所有在线用户,清除对应Session(若使用分布式缓存如Redis,可直接删除缓存键)
foreach (var session in HttpContext.Current.ApplicationInstance.Context.Session)
{
var sessionUserId = session["UserId"] as string;
if (userIds.Contains(sessionUserId))
{
session.Remove("UserPermissions");
}
}
}
}
}5.3 控制器/Action级别的权限标记#
在需要验证的控制器或Action上添加自定义权限标记:
[CustomAuthorize("Role_View")]
public class RoleController : Controller
{
// ...
[CustomAuthorize("Role_AssignPerm")]
public ActionResult AssignPermissions(int roleId)
{
// ...
}
}6. 最佳实践与常见问题#
6.1 最佳实践#
- 权限Code规范:采用
模块名_操作名的命名方式(如User_Create),便于识别与维护。 - 权限缓存:将用户权限缓存到Session或Redis,避免每次请求都查询数据库,提升性能。
- 角色优先原则:尽量通过角色分配权限,而非直接给用户分配权限,简化权限管理。
- 超级角色预设:为超级管理员角色自动分配所有权限,避免手动配置。
- 按钮级权限控制:在视图中根据用户权限动态显示/隐藏操作按钮:
@if (PermissionHelper.GetCurrentUserPermissions(HttpContext).Contains("User_Create")) { <button class="btn btn-success" onclick="addUser()">新增用户</button> }
6.2 常见问题#
Q1:权限分配后不生效?#
A:检查是否已清除用户的权限缓存。可在权限分配成功后,调用PermissionHelper.ClearUserPermissionCacheByRole(roleId)清除该角色下所有用户的缓存。
Q2:如何处理匿名访问的Action?#
A:在Action上标记[AllowAnonymous],自定义AuthorizeAttribute会自动忽略该Action的验证。
Q3:如何实现模块级的权限控制?#
A:在控制器级别添加[CustomAuthorize]标记,并传入模块对应的多个权限Code,或在自定义Attribute中支持多权限验证。
7. 本章总结与下章预告#
本章总结#
- 完成了角色、权限及关联关系的数据库设计
- 实现了角色的增删改查功能
- 构建了角色权限分配的完整逻辑
- 自定义了权限拦截器,实现了MVC中的权限验证
下章预告#
下一章我们将实现用户与角色的关联分配,并讲解如何根据用户权限动态生成系统菜单,进一步完善权限体系。