无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(9) 角色管理,分配权限

在前面的章节中,我们已经完成了ASP.NET MVC项目的基础框架搭建、用户登录模块的实现。作为权限管理系统的核心环节,角色管理与权限分配是实现系统资源访问控制的关键——通过角色将权限批量赋予用户,既能简化权限管控流程,又能保障系统的安全性。本章将详细讲解角色的CRUD操作、权限分配的核心逻辑,以及如何在MVC中实现权限验证拦截,帮助你构建完整的权限体系。

目录#

  1. 前置回顾与本章目标
  2. 角色与权限的数据库设计优化
  3. 角色管理模块的功能实现 3.1 角色列表展示(Index) 3.2 角色新增/编辑(Create/Edit) 3.3 角色删除(Delete)
  4. 权限分配核心实现 4.1 权限数据的加载与分组展示 4.2 权限分配的逻辑处理 4.3 权限持久化到数据库
  5. ASP.NET MVC中的权限验证拦截 5.1 自定义AuthorizeAttribute 5.2 控制器/Action级别的权限标记
  6. 最佳实践与常见问题
  7. 本章总结与下章预告
  8. 参考资料

1. 前置回顾与本章目标#

前置回顾#

  • 已完成ASP.NET MVC项目基础框架(路由、EF上下文、布局页)
  • 实现了用户登录、身份认证功能
  • 数据库已有用户表(ApplicationUser)

本章目标#

  1. 设计并实现角色、权限及关联关系的数据库模型
  2. 完成角色的增删改查(CRUD)功能
  3. 实现角色与权限的分配逻辑
  4. 自定义权限拦截器,实现控制器/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 最佳实践#

  1. 权限Code规范:采用模块名_操作名的命名方式(如User_Create),便于识别与维护。
  2. 权限缓存:将用户权限缓存到Session或Redis,避免每次请求都查询数据库,提升性能。
  3. 角色优先原则:尽量通过角色分配权限,而非直接给用户分配权限,简化权限管理。
  4. 超级角色预设:为超级管理员角色自动分配所有权限,避免手动配置。
  5. 按钮级权限控制:在视图中根据用户权限动态显示/隐藏操作按钮:
    @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. 本章总结与下章预告#

本章总结#

  1. 完成了角色、权限及关联关系的数据库设计
  2. 实现了角色的增删改查功能
  3. 构建了角色权限分配的完整逻辑
  4. 自定义了权限拦截器,实现了MVC中的权限验证

下章预告#

下一章我们将实现用户与角色的关联分配,并讲解如何根据用户权限动态生成系统菜单,进一步完善权限体系。


8. 参考资料#

  1. ASP.NET MVC AuthorizeAttribute 官方文档
  2. Entity Framework 关联表配置
  3. ASP.NET MVC 自定义过滤器
  4. ASP.NET MVC 数据验证