无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(6)控制器基类——统一处理登录用户、权限认证与日志记录

在ASP.NET MVC项目开发中,控制器(Controller)是处理用户请求的核心组件。随着项目规模扩大,我们会发现多个控制器存在大量重复逻辑:例如获取当前登录用户信息、验证用户权限、记录操作日志等。如果每个控制器都单独实现这些逻辑,不仅会导致代码冗余,还会增加后期维护成本。

控制器基类(BaseController) 正是为解决这一问题而生。它将所有控制器的共性逻辑(如用户认证、权限检查、日志记录)抽象出来,实现“一次编写,多处复用”,同时保证项目代码的一致性和可维护性。

本文将从“为什么需要基类”出发,详细讲解控制器基类的核心职责、实现步骤、最佳实践,并通过实战示例演示如何在项目中落地。

目录#

  1. 什么是控制器基类?为什么需要它?
  2. 控制器基类的核心职责
    • 2.1 登录用户信息管理
    • 2.2 权限认证与访问控制
    • 2.3 统一日志记录
  3. 从零实现控制器基类
    • 3.1 创建 BaseController 基类
    • 3.2 依赖注入:注入日志与用户服务
    • 3.3 实现 CurrentUser:获取当前登录用户信息
    • 3.4 权限认证:基于 Action Filter 的访问控制
    • 3.5 统一日志记录:Action 生命周期日志
    • 3.6 异常处理与错误日志
  4. 最佳实践与避坑指南
    • 4.1 依赖注入解耦,避免硬编码
    • 4.2 支持异步 Action,避免阻塞
    • 4.3 控制基类职责范围,避免过度封装
    • 4.4 测试策略:如何测试基类逻辑
  5. 实战示例:使用 BaseController 开发业务控制器
    • 5.1 派生控制器示例(HomeController)
    • 5.2 权限控制示例([AuthorizePermission] 特性)
    • 5.3 日志输出效果验证
  6. 总结
  7. 参考资料

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)模拟 IUserServiceILogger 等依赖,验证权限检查、日志记录逻辑是否正确。
  • 集成测试:创建测试控制器继承 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] 特性)#

  • 场景MyOrders Action 标记了 [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 过滤器方法(OnActionExecutingOnException)和自定义特性([AuthorizePermission])实现逻辑;
  • 最佳实践:依赖注入解耦、支持异步 Action、控制职责范围。

合理使用控制器基类,能显著提升项目代码质量,减少重复劳动,为后续功能扩展奠定坚实基础。

7. 参考资料#