无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(7.1) 模块管理,验证权限,展示模块列表

在企业级Web应用中,模块管理权限验证是保障系统安全性、可扩展性的核心功能。模块管理负责组织系统的功能菜单(如用户管理、角色管理、订单管理等),而权限验证则控制不同角色的用户是否能访问指定模块。本章将基于已搭建的ASP.NET MVC基础框架(含用户、角色体系),从零实现模块管理的完整流程:数据库设计、权限验证核心逻辑、模块列表展示及通用业务功能。

目录#

  1. 前置准备与环境要求
  2. 模块管理核心数据库设计
  3. 权限验证核心实现(自定义过滤器)
  4. 模块管理业务逻辑层开发
  5. 模块列表展示(控制器+视图+分页搜索)
  6. 功能测试与异常处理
  7. 最佳实践与扩展建议
  8. 参考资料

1. 前置准备与环境要求#

1.1 环境依赖#

  • 开发工具:Visual Studio 2019/2022
  • .NET Framework版本:4.7.2及以上
  • ORM框架:Entity Framework 6.x(代码优先/数据库优先均可)
  • 基础框架:已实现用户(User)、角色(Role)表及登录认证功能
  • 前端组件:Bootstrap 3.x/4.x、JQuery、zTree(树形模块展示可选)

1.2 基础假设#

本文假设您已完成以下基础功能:

  • 用户登录/退出逻辑
  • 角色与用户的关联(多对多关系)
  • 身份认证机制(FormsAuthentication或ASP.NET Identity)

2. 模块管理核心数据库设计#

模块管理的核心是模块表角色-模块关联表,用于存储模块结构及角色权限映射。

2.1 数据库表结构#

2.1.1 模块表(Module)#

字段名类型说明
IdINT(主键)模块ID
ModuleNameNVARCHAR(50)模块名称(如"用户管理")
ParentIdINT父模块ID(0表示根模块)
UrlPathNVARCHAR(100)模块访问路径(如"/User/Index")
IconClassNVARCHAR(50)菜单图标(如"fa fa-user")
SortOrderINT排序号(升序排列)
IsEnabledBIT是否启用(1=启用,0=禁用)
CreateTimeDATETIME创建时间

2.1.2 角色-模块关联表(RoleModule)#

用于实现角色与模块的多对多权限映射:

字段名类型说明
RoleIdINT(外键)角色ID
ModuleIdINT(外键)模块ID

2.2 实体类实现(Code First)#

// 模块实体类
public class Module
{
    public int Id { get; set; }
    [Required]
    [StringLength(50)]
    public string ModuleName { get; set; }
    public int ParentId { get; set; }
    [StringLength(100)]
    public string UrlPath { get; set; }
    [StringLength(50)]
    public string IconClass { get; set; }
    public int SortOrder { get; set; }
    public bool IsEnabled { get; set; }
    public DateTime CreateTime { get; set; } = DateTime.Now;
 
    // 导航属性:子模块
    public virtual ICollection<Module> Children { get; set; } = new List<Module>();
    // 导航属性:关联的角色
    public virtual ICollection<Role> Roles { get; set; } = new List<Role>();
}
 
// 角色实体类(已存在的基础上补充模块导航属性)
public class Role
{
    public int Id { get; set; }
    [Required]
    [StringLength(50)]
    public string RoleName { get; set; }
    // 导航属性:关联的模块
    public virtual ICollection<Module> Modules { get; set; } = new List<Module>();
}
 
// DbContext配置
public class AppDbContext : DbContext
{
    public DbSet<Module> Modules { get; set; }
    public DbSet<Role> Roles { get; set; }
    public DbSet<RoleModule> RoleModules { get; set; }
 
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // 配置多对多关系
        modelBuilder.Entity<Role>()
            .HasMany(r => r.Modules)
            .WithMany(m => m.Roles)
            .Map(t => t.MapLeftKey("RoleId")
                        .MapRightKey("ModuleId")
                        .ToTable("RoleModule"));
 
        // 模块表默认排序
        modelBuilder.Entity<Module>()
            .Property(m => m.SortOrder)
            .HasDefaultValue(0);
    }
}

3. 权限验证核心实现(自定义过滤器)#

ASP.NET MVC的ActionFilterAttribute是实现权限验证的最佳方式,我们将自定义一个PermissionFilter,在请求执行前验证用户权限。

3.1 权限存储策略#

推荐使用**Claim(声明式身份验证)**存储用户权限,而非Session,因为Claim是ASP.NET身份验证的标准方式,支持分布式场景。

  • 用户登录时,将其所属角色的所有模块权限存入Claim:
// 登录逻辑中获取用户权限并添加到Claim
var user = _userService.GetUserByAccount(account);
var userModules = _moduleService.GetUserModules(user.Id);
 
// 创建身份凭证
var identity = new ClaimsIdentity(DefaultAuthenticationTypes.ApplicationCookie);
// 添加用户ID、角色等基础声明
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));
// 添加模块权限声明(格式:"Permission:模块路径")
foreach (var module in userModules)
{
    if (!string.IsNullOrEmpty(module.UrlPath))
    {
        identity.AddClaim(new Claim("Permission", module.UrlPath));
    }
}
 
// 登录
AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = rememberMe }, identity);

3.2 自定义权限过滤器#

/// <summary>
/// 权限验证过滤器
/// </summary>
public class PermissionFilter : ActionFilterAttribute
{
    // 需要验证的模块路径
    public string ModulePath { get; set; }
 
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        // 1. 跳过匿名访问的Action(如登录页)
        var allowAnonymous = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true) ||
                             filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true);
        if (allowAnonymous)
        {
            base.OnActionExecuting(filterContext);
            return;
        }
 
        // 2. 验证用户是否登录
        var user = filterContext.HttpContext.User;
        if (!user.Identity.IsAuthenticated)
        {
            filterContext.Result = new RedirectResult("/Account/Login");
            return;
        }
 
        // 3. 验证用户是否拥有当前模块权限
        var hasPermission = user.HasClaim("Permission", ModulePath);
        if (!hasPermission)
        {
            // 无权限时的处理:跳转到无权限页面或返回JSON
            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                filterContext.Result = new JsonResult
                {
                    Data = new { Success = false, Message = "无权限访问该模块" },
                    JsonRequestBehavior = JsonRequestBehavior.AllowGet
                };
            }
            else
            {
                filterContext.Result = new ViewResult { ViewName = "NoPermission" };
            }
        }
 
        base.OnActionExecuting(filterContext);
    }
}

3.3 过滤器使用示例#

在需要验证权限的Action上添加特性:

[PermissionFilter(ModulePath = "/User/Index")]
public ActionResult Index()
{
    return View();
}

4. 模块管理业务逻辑层开发#

封装模块管理的核心业务逻辑,提供模块增删改查、树形结构获取、用户权限查询等功能。

4.1 模块服务接口#

public interface IModuleService
{
    // 获取所有模块(树形结构)
    List<Module> GetModuleTree();
    // 获取用户拥有的模块列表
    List<Module> GetUserModules(int userId);
    // 添加/编辑模块
    bool SaveModule(Module module);
    // 删除模块(含子模块)
    bool DeleteModule(int moduleId);
    // 分页查询模块列表
    IPagedList<Module> GetModulePageList(int pageIndex, int pageSize, string keyword = "");
}

4.2 服务实现类核心代码#

public class ModuleService : IModuleService
{
    private readonly AppDbContext _dbContext;
 
    public ModuleService(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }
 
    // 获取树形模块结构
    public List<Module> GetModuleTree()
    {
        var allModules = _dbContext.Modules.Where(m => m.IsEnabled).OrderBy(m => m.SortOrder).ToList();
        // 递归构建树形结构
        var rootModules = allModules.Where(m => m.ParentId == 0).ToList();
        foreach (var root in rootModules)
        {
            BuildModuleTree(root, allModules);
        }
        return rootModules;
    }
 
    // 递归构建子模块
    private void BuildModuleTree(Module parentModule, List<Module> allModules)
    {
        var children = allModules.Where(m => m.ParentId == parentModule.Id).ToList();
        parentModule.Children = children;
        foreach (var child in children)
        {
            BuildModuleTree(child, allModules);
        }
    }
 
    // 分页查询模块列表
    public IPagedList<Module> GetModulePageList(int pageIndex, int pageSize, string keyword = "")
    {
        var query = _dbContext.Modules.AsQueryable();
        if (!string.IsNullOrEmpty(keyword))
        {
            query = query.Where(m => m.ModuleName.Contains(keyword));
        }
        return query.OrderBy(m => m.SortOrder).ToPagedList(pageIndex, pageSize);
    }
}

5. 模块列表展示(控制器+视图)#

实现模块列表的分页、搜索、树形展示等通用功能。

5.1 控制器代码#

public class ModuleController : Controller
{
    private readonly IModuleService _moduleService;
 
    public ModuleController(IModuleService moduleService)
    {
        _moduleService = moduleService;
    }
 
    // 模块列表页
    [PermissionFilter(ModulePath = "/Module/Index")]
    public ActionResult Index(int page = 1, string keyword = "")
    {
        const int pageSize = 10;
        var moduleList = _moduleService.GetModulePageList(page, pageSize, keyword);
        ViewBag.Keyword = keyword;
        return View(moduleList);
    }
 
    // 加载树形模块(用于角色权限分配)
    public JsonResult GetModuleTree()
    {
        var tree = _moduleService.GetModuleTree();
        return Json(tree, JsonRequestBehavior.AllowGet);
    }
}

5.2 视图代码(Razor)#

使用Bootstrap表格和PagedList.Mvc实现分页:

@using PagedList.Mvc
@model IPagedList<Module>
 
<div class="container">
    <h3>模块管理</h3>
    <!-- 搜索栏 -->
    <div class="row mb-3">
        <div class="col-md-8">
            <form action="@Url.Action("Index")" method="get">
                <div class="input-group">
                    <input type="text" name="keyword" class="form-control" placeholder="搜索模块名称" value="@ViewBag.Keyword">
                    <span class="input-group-btn">
                        <button class="btn btn-primary" type="submit">搜索</button>
                    </span>
                </div>
            </form>
        </div>
        <div class="col-md-4 text-right">
            <a href="@Url.Action("Create")" class="btn btn-success">添加模块</a>
        </div>
    </div>
 
    <!-- 模块列表 -->
    <table class="table table-bordered table-striped">
        <thead>
            <tr>
                <th>模块名称</th>
                <th>父模块</th>
                <th>访问路径</th>
                <th>排序</th>
                <th>状态</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model)
            {
                <tr>
                    <td>@item.ModuleName</td>
                    <td>@(item.ParentId == 0 ? "根模块" : _moduleService.GetModuleById(item.ParentId)?.ModuleName)</td>
                    <td>@item.UrlPath</td>
                    <td>@item.SortOrder</td>
                    <td>@(item.IsEnabled ? "启用" : "禁用")</td>
                    <td>
                        <a href="@Url.Action("Edit", new { id = item.Id })" class="btn btn-sm btn-info">编辑</a>
                        <a href="@Url.Action("Delete", new { id = item.Id })" class="btn btn-sm btn-danger" onclick="return confirm('确定删除该模块吗?')">删除</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
 
    <!-- 分页控件 -->
    <div class="text-center">
        @Html.PagedListPager(Model, page => Url.Action("Index", new { page, keyword = ViewBag.Keyword }),
            new PagedListRenderOptions { LiElementClasses = new[] { "page-item" }, PageClasses = new[] { "page-link" } })
    </div>
</div>

6. 功能测试与异常处理#

6.1 测试流程#

  1. 添加测试模块:添加"用户管理"、"角色管理"等根模块及子模块
  2. 角色权限分配:给管理员角色分配所有模块权限,给普通用户分配部分模块权限
  3. 权限验证测试
    • 管理员登录:可访问所有模块
    • 普通用户登录:尝试访问无权限模块,验证是否跳转至无权限页面
  4. 分页搜索测试:验证模块列表的分页、搜索功能是否正常

6.2 异常处理#

  • 在过滤器中捕获权限验证异常,统一返回友好提示
  • 在服务层添加try-catch块,记录异常日志(如使用NLog、Log4Net)
  • 模块删除时,需判断是否有子模块或关联的角色,避免数据不一致

7. 最佳实践与扩展建议#

7.1 最佳实践#

  1. 声明式权限优先:使用Claim而非Session存储权限,符合ASP.NET身份验证标准
  2. 权限粒度控制:支持模块级、按钮级(如添加/编辑/删除按钮)权限验证,可在Claim中存储更细粒度的权限(如"Permission:Module:Edit")
  3. 树形结构优化:模块树的构建可缓存到Redis或MemoryCache,避免每次请求都查询数据库
  4. 统一过滤器注册:可将PermissionFilter注册为全局过滤器,无需在每个Action上手动添加,通过特性标记无需验证的Action
  5. 前端权限控制:除了后端验证,前端菜单也应根据用户权限动态渲染,避免无效菜单展示

7.2 扩展方向#

  • 按钮级权限:在模块表中添加按钮权限字段,或新增Permission表存储细粒度权限
  • 数据级权限:根据用户角色限制可查看的数据范围(如部门管理员只能查看本部门数据)
  • 权限日志:记录用户的权限访问日志,便于审计
  • 动态权限更新:支持用户权限实时更新,无需重新登录

8. 参考资料#

  1. ASP.NET MVC 过滤器官方文档
  2. ASP.NET 声明式身份验证
  3. Entity Framework 多对多关系配置
  4. PagedList.Mvc 分页组件文档