无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(6)控制器基类——统一处理登录用户、权限认证与日志记录
在ASP.NET MVC项目开发中,控制器(Controller)是处理用户请求的核心组件。随着项目规模扩大,我们会发现多个控制器存在大量重复逻辑:例如获取当前登录用户信息、验证用户权限、记录操作日志等。如果每个控制器都单独实现这些逻辑,不仅会导致代码冗余,还会增加后期维护成本。
控制器基类(BaseController) 正是为解决这一问题而生。它将所有控制器的共性逻辑(如用户认证、权限检查、日志记录)抽象出来,实现“一次编写,多处复用”,同时保证项目代码的一致性和可维护性。
本文将从“为什么需要基类”出发,详细讲解控制器基类的核心职责、实现步骤、最佳实践,并通过实战示例演示如何在项目中落地。
目录#
- 什么是控制器基类?为什么需要它?
- 控制器基类的核心职责
- 2.1 登录用户信息管理
- 2.2 权限认证与访问控制
- 2.3 统一日志记录
- 从零实现控制器基类
- 3.1 创建 BaseController 基类
- 3.2 依赖注入:注入日志与用户服务
- 3.3 实现 CurrentUser:获取当前登录用户信息
- 3.4 权限认证:基于 Action Filter 的访问控制
- 3.5 统一日志记录:Action 生命周期日志
- 3.6 异常处理与错误日志
- 最佳实践与避坑指南
- 4.1 依赖注入解耦,避免硬编码
- 4.2 支持异步 Action,避免阻塞
- 4.3 控制基类职责范围,避免过度封装
- 4.4 测试策略:如何测试基类逻辑
- 实战示例:使用 BaseController 开发业务控制器
- 5.1 派生控制器示例(HomeController)
- 5.2 权限控制示例([AuthorizePermission] 特性)
- 5.3 日志输出效果验证
- 总结
- 参考资料
1. 什么是控制器基类?为什么需要它?#
1.1 没有基类的痛点#
在未使用基类时,每个控制器都需要单独处理以下逻辑:
- 用户信息获取:在每个 Action 中通过
HttpContext.User解析用户 ID、角色等信息; - 权限验证:在 Action 开头重复编写“检查用户是否有权限访问”的代码;
- 日志记录:在每个 Action 中手动记录“操作开始/结束”“异常信息”等日志。
例如,一个普通的 HomeController 可能会这样写:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IUserService _userService;
public HomeController(ILogger<HomeController> logger, IUserService userService)
{
_logger = logger;
_userService = userService;
}
public ActionResult Index()
{
// 1. 获取当前用户信息
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login", "Account");
}
var user = _userService.GetUserById(userId);
if (user == null)
{
return RedirectToAction("Login", "Account");
}
// 2. 权限验证(假设需要 "Home.View" 权限)
if (!_userService.HasPermission(user.Id, "Home.View"))
{
return new HttpStatusCodeResult(403, "无访问权限");
}
// 3. 记录日志
_logger.LogInformation($"用户 {user.UserName} 访问了首页");
// 业务逻辑...
return View();
}
}如果项目中有 10 个、20 个控制器,每个控制器的每个 Action 都重复上述代码,会导致:
- 代码冗余:相同逻辑重复编写,违反 DRY(Don't Repeat Yourself)原则;
- 维护困难:若权限逻辑变更(如新增权限类型),需修改所有控制器;
- 一致性差:不同开发者可能实现方式不同,导致项目风格混乱。
1.2 基类的核心价值#
控制器基类(BaseController)通过继承机制,将上述共性逻辑抽象到父类中,派生控制器只需专注于业务逻辑。其核心价值包括:
- 代码复用:共性逻辑(用户信息、权限、日志)在基类中统一实现;
- 集中维护:逻辑变更只需修改基类,无需改动所有派生控制器;
- 规范约束:强制所有控制器遵循统一的用户认证、权限检查流程。
2. 控制器基类的核心职责#
控制器基类的核心是处理跨控制器的共性需求,主要包括以下三类:
2.1 登录用户信息管理#
在用户登录后,ASP.NET 会将用户身份信息(如用户 ID、用户名、角色)存储在 HttpContext.User 中(通常基于 Cookie 或 JWT Token)。基类需提供便捷的方式获取这些信息,避免在每个 Action 中重复解析。
核心需求:
- 获取当前登录用户的基本信息(ID、用户名、角色等);
- 若用户未登录,自动跳转到登录页或返回未授权响应。
2.2 权限认证与访问控制#
并非所有用户都能访问所有功能(如普通用户不能访问管理员后台)。基类需统一实现权限检查逻辑,确保只有授权用户才能执行特定 Action。
核心需求:
- 支持通过特性(Attribute)标记 Action 所需的权限;
- 在 Action 执行前自动检查用户是否拥有所需权限;
- 无权限时返回 403 或自定义提示。
2.3 统一日志记录#
日志是排查问题、审计操作的重要依据。基类需在 Action 执行的关键节点(开始、结束、异常)自动记录日志,无需手动编写。
核心需求:
- 记录 Action 执行开始(包含用户、参数等信息);
- 记录 Action 执行结束(包含耗时、结果等信息);
- 记录 Action 执行异常(包含异常详情、堆栈信息)。
3. 从零实现控制器基类#
下面我们逐步实现一个功能完整的 BaseController,涵盖上述核心职责。
3.1 创建 BaseController 基类#
首先,创建一个继承自 Controller 的基类 BaseController:
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
// 基类继承自 Controller
public class BaseController : Controller
{
// 后续会添加构造函数、属性和方法
}3.2 依赖注入:注入日志与用户服务#
基类需要依赖外部服务(如日志服务、用户权限服务),通过构造函数注入获取:
// 日志服务(记录操作日志和异常)
protected readonly ILogger<BaseController> _logger;
// 用户服务(获取用户信息、验证权限)
protected readonly IUserService _userService;
// 构造函数注入依赖
public BaseController(ILogger<BaseController> logger, IUserService userService)
{
_logger = logger;
_userService = userService;
}说明:
IUserService是自定义接口,需包含获取用户信息(GetUserById)和验证权限(HasPermission)的方法,后续会给出示例。
3.3 实现 CurrentUser:获取当前登录用户信息#
定义 CurrentUser 属性,自动解析 HttpContext.User 中的用户信息,并缓存结果(避免重复解析):
// 当前登录用户信息(延迟加载,首次访问时解析)
private UserInfo _currentUser;
protected UserInfo CurrentUser
{
get
{
if (_currentUser != null) return _currentUser;
// 从 HttpContext.User 中解析用户 ID(假设登录时已将用户 ID 存入 ClaimTypes.NameIdentifier)
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrEmpty(userId))
{
// 未登录:抛出异常或返回 null,根据业务需求处理
_logger.LogWarning("当前用户未登录");
return null;
}
// 从用户服务获取完整用户信息(包含角色、权限等)
_currentUser = _userService.GetUserById(userId);
if (_currentUser == null)
{
_logger.LogError($"用户 ID {userId} 不存在");
return null;
}
return _currentUser;
}
}
// 用户信息模型(示例)
public class UserInfo
{
public string Id { get; set; } // 用户 ID
public string UserName { get; set; } // 用户名
public List<string> Roles { get; set; } // 角色列表
public List<string> Permissions { get; set; } // 权限列表
}
// IUserService 接口(示例)
public interface IUserService
{
UserInfo GetUserById(string userId);
bool HasPermission(string userId, string permission);
}使用场景:派生控制器可直接通过 CurrentUser 获取用户信息,无需重复解析:
public class HomeController : BaseController
{
public HomeController(ILogger<BaseController> logger, IUserService userService)
: base(logger, userService) { }
public IActionResult Index()
{
// 直接使用基类的 CurrentUser 属性
ViewBag.UserName = CurrentUser?.UserName;
return View();
}
}3.4 权限认证:基于 Action Filter 的访问控制#
通过自定义特性([AuthorizePermission])标记 Action 所需权限,在基类中重写 OnActionExecuting 方法(Action 执行前触发),自动检查权限。
步骤 1:定义权限特性 AuthorizePermissionAttribute#
// 用于标记 Action 所需权限的特性
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class AuthorizePermissionAttribute : Attribute
{
public string Permission { get; }
// 构造函数:传入所需权限(如 "Home.View")
public AuthorizePermissionAttribute(string permission)
{
Permission = permission;
}
}步骤 2:在基类中重写 OnActionExecuting 检查权限#
public override void OnActionExecuting(ActionExecutingContext context)
{
// 1. 先执行基类逻辑(必须调用,确保 MVC 框架正常工作)
base.OnActionExecuting(context);
// 2. 检查用户是否已登录
if (CurrentUser == null)
{
// 未登录:跳转到登录页(或返回 401)
context.Result = new RedirectToActionResult("Login", "Account", null);
return;
}
// 3. 获取当前 Action 标记的权限需求
var requiredPermissions = context.ActionDescriptor.EndpointMetadata
.OfType<AuthorizePermissionAttribute>()
.Select(attr => attr.Permission)
.ToList();
// 4. 若无需权限,直接放行
if (!requiredPermissions.Any()) return;
// 5. 检查用户是否拥有所有 requiredPermissions
var hasAllPermissions = requiredPermissions.All(perm =>
_userService.HasPermission(CurrentUser.Id, perm)
);
if (!hasAllPermissions)
{
// 无权限:返回 403 Forbidden
context.Result = new ForbidResult();
_logger.LogWarning($"用户 {CurrentUser.UserName} 尝试访问无权限资源:{string.Join(",", requiredPermissions)}");
}
}工作原理:
- 当 Action 被请求时,MVC 框架会先执行
OnActionExecuting; - 基类在此方法中检查用户登录状态和权限;
- 若未登录或无权限,直接设置
context.Result(如跳转登录页、403),阻止 Action 继续执行。
3.5 统一日志记录:Action 生命周期日志#
通过重写 OnActionExecuting(执行前)和 OnActionExecuted(执行后)方法,记录 Action 执行的关键信息:
// Action 执行前日志
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
// 记录 Action 开始执行(包含控制器名、Action 名、用户信息、参数)
var controllerName = context.ActionDescriptor.ControllerName;
var actionName = context.ActionDescriptor.ActionName;
var parameters = string.Join(", ", context.ActionArguments.Select(kv => $"{kv.Key}={kv.Value}"));
_logger.LogInformation(
"Action 开始执行:Controller={Controller}, Action={Action}, User={User}, Parameters={Parameters}",
controllerName, actionName, CurrentUser?.UserName ?? "未登录用户", parameters
);
}
// Action 执行后日志
public override void OnActionExecuted(ActionExecutedContext context)
{
base.OnActionExecuted(context);
var controllerName = context.ActionDescriptor.ControllerName;
var actionName = context.ActionDescriptor.ActionName;
// 记录 Action 执行结束(包含是否成功、耗时)
if (context.Exception == null)
{
_logger.LogInformation(
"Action 执行成功:Controller={Controller}, Action={Action}, User={User}",
controllerName, actionName, CurrentUser?.UserName ?? "未登录用户"
);
}
else
{
// 异常已在 OnException 中处理,此处仅记录执行失败
_logger.LogError(
"Action 执行失败:Controller={Controller}, Action={Action}, User={User}",
controllerName, actionName, CurrentUser?.UserName ?? "未登录用户"
);
}
}3.6 异常处理与错误日志#
重写 OnException 方法,捕获 Action 执行过程中的未处理异常,并记录详细日志:
public override void OnException(ExceptionContext context)
{
base.OnException(context);
var exception = context.Exception;
var controllerName = context.ActionDescriptor.ControllerName;
var actionName = context.ActionDescriptor.ActionName;
// 记录异常详情(包含堆栈跟踪)
_logger.LogError(
exception,
"Action 执行异常:Controller={Controller}, Action={Action}, User={User}, Message={Message}",
controllerName, actionName, CurrentUser?.UserName ?? "未登录用户", exception.Message
);
// 标记异常已处理,避免 MVC 框架默认返回 500 错误页(可选)
context.ExceptionHandled = true;
// 返回自定义错误响应(如 JSON 或错误视图)
if (Request.Headers["Accept"].Contains("application/json"))
{
context.Result = new JsonResult(new {
success = false,
message = "操作失败:" + exception.Message
}) { StatusCode = 500 };
}
else
{
context.Result = View("Error", new ErrorViewModel { Message = exception.Message });
}
}4. 最佳实践与避坑指南#
4.1 依赖注入解耦,避免硬编码#
- 避免在基类中直接实例化服务(如
new UserService()),应通过构造函数注入接口(如IUserService),便于单元测试和服务替换。 - 示例:
IUserService可有不同实现(如数据库实现、缓存实现),通过 DI 容器切换,无需修改基类。
4.2 支持异步 Action,避免阻塞#
若项目中使用异步 Action(async/await),需重写异步版本的过滤器方法:
// 异步 Action 执行前(替代 OnActionExecuting)
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 执行同步逻辑(如权限检查)
OnActionExecuting(context);
if (context.Result != null)
{
// 若已设置 Result(如跳转、403),直接返回
return;
}
// 执行后续逻辑(包括 Action 本身)
var resultContext = await next();
// 执行 Action 后逻辑(如日志记录)
OnActionExecuted(resultContext);
}4.3 控制基类职责范围,避免过度封装#
基类应只处理跨控制器的共性逻辑(如认证、日志),业务逻辑(如数据查询、表单验证)应放在派生控制器或服务层。过度封装会导致基类臃肿,难以维护。
4.4 测试策略:如何测试基类逻辑#
- 单元测试:通过 Mock 框架(如 Moq)模拟
IUserService、ILogger等依赖,验证权限检查、日志记录逻辑是否正确。 - 集成测试:创建测试控制器继承
BaseController,模拟 HTTP 请求,验证登录跳转、权限拦截等场景是否符合预期。
5. 实战示例:使用 BaseController 开发业务控制器#
5.1 派生控制器示例(HomeController)#
public class HomeController : BaseController
{
// 构造函数需调用基类构造函数,传入依赖
public HomeController(ILogger<BaseController> logger, IUserService userService)
: base(logger, userService) { }
// 无需权限的 Action(如首页)
public IActionResult Index()
{
ViewBag.UserName = CurrentUser?.UserName; // 使用基类的 CurrentUser
return View();
}
// 需要 "Order.View" 权限的 Action
[AuthorizePermission("Order.View")]
public IActionResult MyOrders()
{
// 业务逻辑:查询当前用户的订单
var orders = _orderService.GetOrdersByUserId(CurrentUser.Id);
return View(orders);
}
// 异步 Action 示例
[AuthorizePermission("User.Edit")]
public async Task<IActionResult> EditProfile()
{
var user = await _userService.GetUserByIdAsync(CurrentUser.Id);
return View(user);
}
}5.2 权限控制示例([AuthorizePermission] 特性)#
- 场景:
MyOrdersAction 标记了[AuthorizePermission("Order.View")],基类会自动检查当前用户是否拥有该权限。 - 无权限时:基类返回 403 Forbidden,日志记录:
用户 zhangsan 尝试访问无权限资源:Order.View。
5.3 日志输出效果#
执行 MyOrders Action 后,日志输出如下:
info: BaseController[0]
Action 开始执行:Controller=Home, Action=MyOrders, User=zhangsan, Parameters=
info: BaseController[0]
Action 执行成功:Controller=Home, Action=MyOrders, User=zhangsan
若 Action 抛出异常,日志输出:
fail: BaseController[0]
Action 执行异常:Controller=Home, Action=MyOrders, User=zhangsan, Message=数据库连接失败
System.Exception: 数据库连接失败
at HomeController.MyOrders() in ...
6. 总结#
控制器基类是 ASP.NET MVC 项目中“降本增效”的关键组件,通过抽象共性逻辑(用户信息、权限认证、日志记录),实现了代码复用和集中维护。本文从需求分析到代码实现,详细讲解了基类的设计思路和落地步骤,核心要点包括:
- 核心职责:统一处理登录用户信息、权限控制、日志记录;
- 实现关键:通过重写 Action 过滤器方法(
OnActionExecuting、OnException)和自定义特性([AuthorizePermission])实现逻辑; - 最佳实践:依赖注入解耦、支持异步 Action、控制职责范围。
合理使用控制器基类,能显著提升项目代码质量,减少重复劳动,为后续功能扩展奠定坚实基础。