无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(7.1) 模块管理,验证权限,展示模块列表
在企业级Web应用中,模块管理与权限验证是保障系统安全性、可扩展性的核心功能。模块管理负责组织系统的功能菜单(如用户管理、角色管理、订单管理等),而权限验证则控制不同角色的用户是否能访问指定模块。本章将基于已搭建的ASP.NET MVC基础框架(含用户、角色体系),从零实现模块管理的完整流程:数据库设计、权限验证核心逻辑、模块列表展示及通用业务功能。
目录#
- 前置准备与环境要求
- 模块管理核心数据库设计
- 权限验证核心实现(自定义过滤器)
- 模块管理业务逻辑层开发
- 模块列表展示(控制器+视图+分页搜索)
- 功能测试与异常处理
- 最佳实践与扩展建议
- 参考资料
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)#
| 字段名 | 类型 | 说明 |
|---|---|---|
| Id | INT(主键) | 模块ID |
| ModuleName | NVARCHAR(50) | 模块名称(如"用户管理") |
| ParentId | INT | 父模块ID(0表示根模块) |
| UrlPath | NVARCHAR(100) | 模块访问路径(如"/User/Index") |
| IconClass | NVARCHAR(50) | 菜单图标(如"fa fa-user") |
| SortOrder | INT | 排序号(升序排列) |
| IsEnabled | BIT | 是否启用(1=启用,0=禁用) |
| CreateTime | DATETIME | 创建时间 |
2.1.2 角色-模块关联表(RoleModule)#
用于实现角色与模块的多对多权限映射:
| 字段名 | 类型 | 说明 |
|---|---|---|
| RoleId | INT(外键) | 角色ID |
| ModuleId | INT(外键) | 模块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 测试流程#
- 添加测试模块:添加"用户管理"、"角色管理"等根模块及子模块
- 角色权限分配:给管理员角色分配所有模块权限,给普通用户分配部分模块权限
- 权限验证测试:
- 管理员登录:可访问所有模块
- 普通用户登录:尝试访问无权限模块,验证是否跳转至无权限页面
- 分页搜索测试:验证模块列表的分页、搜索功能是否正常
6.2 异常处理#
- 在过滤器中捕获权限验证异常,统一返回友好提示
- 在服务层添加try-catch块,记录异常日志(如使用NLog、Log4Net)
- 模块删除时,需判断是否有子模块或关联的角色,避免数据不一致
7. 最佳实践与扩展建议#
7.1 最佳实践#
- 声明式权限优先:使用Claim而非Session存储权限,符合ASP.NET身份验证标准
- 权限粒度控制:支持模块级、按钮级(如添加/编辑/删除按钮)权限验证,可在Claim中存储更细粒度的权限(如"Permission:Module:Edit")
- 树形结构优化:模块树的构建可缓存到Redis或MemoryCache,避免每次请求都查询数据库
- 统一过滤器注册:可将
PermissionFilter注册为全局过滤器,无需在每个Action上手动添加,通过特性标记无需验证的Action - 前端权限控制:除了后端验证,前端菜单也应根据用户权限动态渲染,避免无效菜单展示
7.2 扩展方向#
- 按钮级权限:在模块表中添加按钮权限字段,或新增Permission表存储细粒度权限
- 数据级权限:根据用户角色限制可查看的数据范围(如部门管理员只能查看本部门数据)
- 权限日志:记录用户的权限访问日志,便于审计
- 动态权限更新:支持用户权限实时更新,无需重新登录