【无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(5.4)—— 登录功能的实现:创建与登录用户相关的接口和实现类

登录功能是几乎所有Web系统的核心入口,它不仅关乎用户身份验证的安全性,更直接影响系统的可用性与可维护性。在ASP.NET MVC架构中,通过抽象接口+实现类的方式构建登录逻辑,是遵循依赖倒置原则、实现代码解耦与可扩展的最佳实践。

本章我们将从需求分析入手,逐步完成登录相关接口的抽象、实现类的编写、依赖注入配置,并结合密码加密、登录日志记录等核心逻辑,全方位打造一个安全、可维护的登录服务体系。

目录#

  1. 前置知识回顾与本章目标
  2. 登录功能需求与领域模型梳理
  3. 登录相关接口抽象设计
  4. 接口实现类核心逻辑编写
  5. 依赖注入服务配置
  6. 单元测试示例(最佳实践)
  7. 常见问题与行业最佳实践
  8. 总结与下章预告
  9. 参考资料

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

前置知识#

  • 熟悉ASP.NET MVC 5架构与EF Core数据访问
  • 掌握C#异步编程(async/await
  • 了解依赖倒置原则与依赖注入(DI)概念
  • 知晓密码哈希加密的基本原理

本章目标#

  • 完成登录相关业务接口的抽象设计
  • 实现包含密码验证、登录日志的核心业务逻辑
  • 配置依赖注入实现服务解耦
  • 掌握登录功能的安全与性能最佳实践

2. 登录功能需求与领域模型梳理#

核心需求#

  1. 用户名+密码的身份验证,返回登录结果(成功/失败+错误信息)
  2. 密码必须加密存储与验证,禁止明文传输/存储
  3. 记录登录行为日志(时间、IP、登录状态、备注)
  4. 支持异步操作,提升系统并发处理能力

领域模型定义#

首先定义核心实体类(基于EF Core):

// 用户实体
public class User
{
    public int Id { get; set; }
    public string Username { get; set; } // 用户名(唯一)
    public string Nickname { get; set; } // 昵称
    public string Email { get; set; } // 邮箱
    public string PasswordHash { get; set; } // 密码哈希值(非明文)
    public DateTime CreateTime { get; set; } = DateTime.Now;
    public bool IsEnabled { get; set; } = true; // 是否启用
}
 
// 登录日志实体
public class LoginLog
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string IpAddress { get; set; } // 登录IP
    public DateTime LoginTime { get; set; } = DateTime.Now;
    public bool IsSuccess { get; set; } // 登录是否成功
    public string Remark { get; set; } // 备注(如错误信息)
}

同时定义数据传输对象(DTO),避免直接暴露EF实体的敏感字段:

// 登录结果DTO
public class LoginResult
{
    public bool IsSuccess { get; set; }
    public UserInfoDto UserInfo { get; set; }
    public string ErrorMessage { get; set; }
    public LoginLogDto LoginLog { get; set; }
}
 
// 用户信息DTO
public class UserInfoDto
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Nickname { get; set; }
    public string Email { get; set; }
}
 
// 登录日志DTO
public class LoginLogDto
{
    public int Id { get; set; }
    public DateTime LoginTime { get; set; }
    public string IpAddress { get; set; }
}

3. 登录相关接口抽象设计#

基于依赖倒置原则,我们抽象业务接口,将核心逻辑与具体实现解耦,便于后续替换数据层(如从EF切换到Dapper)或扩展功能。

3.1 用户身份验证接口 IUserAuthService#

负责核心的登录验证、用户信息查询逻辑:

public interface IUserAuthService
{
    /// <summary>
    /// 验证登录信息
    /// </summary>
    /// <param name="username">用户名</param>
    /// <param name="password">原始密码</param>
    /// <param name="ipAddress">客户端IP</param>
    /// <returns>登录结果</returns>
    Task<LoginResult> VerifyLoginAsync(string username, string password, string ipAddress);
 
    /// <summary>
    /// 根据用户名查询用户信息
    /// </summary>
    Task<User> GetUserByUsernameAsync(string username);
}

3.2 登录日志接口 ILoginLogService#

负责登录行为的日志记录,满足审计与排查需求:

public interface ILoginLogService
{
    /// <summary>
    /// 记录登录日志
    /// </summary>
    Task<LoginLog> RecordLoginLogAsync(string username, string ipAddress, bool isSuccess, string remark);
}

4. 接口实现类核心逻辑编写#

4.1 密码加密工具类#

使用行业标准的PasswordHasher(来自Microsoft.AspNetCore.Identity)实现密码哈希与验证,禁止自行设计加密逻辑:

public class PasswordHelper
{
    private static readonly PasswordHasher<User> _passwordHasher = new PasswordHasher<User>();
 
    /// <summary>
    /// 哈希加密密码
    /// </summary>
    public static string HashPassword(User user, string rawPassword)
    {
        return _passwordHasher.HashPassword(user, rawPassword);
    }
 
    /// <summary>
    /// 验证密码哈希
    /// </summary>
    public static bool VerifyPassword(User user, string rawPassword, string hashedPassword)
    {
        var result = _passwordHasher.VerifyHashedPassword(user, hashedPassword, rawPassword);
        return result == PasswordVerificationResult.Success;
    }
}

4.2 登录日志实现类 LoginLogService#

public class LoginLogService : ILoginLogService
{
    private readonly AppDbContext _dbContext;
 
    // 通过构造函数注入EF上下文(依赖注入)
    public LoginLogService(AppDbContext dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }
 
    public async Task<LoginLog> RecordLoginLogAsync(string username, string ipAddress, bool isSuccess, string remark)
    {
        var loginLog = new LoginLog
        {
            Username = username,
            IpAddress = ipAddress,
            IsSuccess = isSuccess,
            Remark = remark
        };
 
        _dbContext.LoginLogs.Add(loginLog);
        await _dbContext.SaveChangesAsync();
        return loginLog;
    }
}

4.3 用户验证实现类 UserAuthService#

整合密码验证、用户查询、日志记录的核心逻辑:

public class UserAuthService : IUserAuthService
{
    private readonly AppDbContext _dbContext;
    private readonly ILoginLogService _loginLogService;
 
    public UserAuthService(AppDbContext dbContext, ILoginLogService loginLogService)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        _loginLogService = loginLogService ?? throw new ArgumentNullException(nameof(loginLogService));
    }
 
    public async Task<LoginResult> VerifyLoginAsync(string username, string password, string ipAddress)
    {
        var result = new LoginResult();
 
        // 1. 参数校验
        if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
        {
            result.IsSuccess = false;
            result.ErrorMessage = "用户名或密码不能为空";
            await _loginLogService.RecordLoginLogAsync(username, ipAddress, false, result.ErrorMessage);
            return result;
        }
 
        // 2. 查询用户
        var user = await GetUserByUsernameAsync(username);
        if (user == null)
        {
            result.IsSuccess = false;
            result.ErrorMessage = "用户名不存在";
            await _loginLogService.RecordLoginLogAsync(username, ipAddress, false, result.ErrorMessage);
            return result;
        }
 
        // 3. 验证用户状态
        if (!user.IsEnabled)
        {
            result.IsSuccess = false;
            result.ErrorMessage = "用户已被禁用";
            await _loginLogService.RecordLoginLogAsync(username, ipAddress, false, result.ErrorMessage);
            return result;
        }
 
        // 4. 验证密码
        if (!PasswordHelper.VerifyPassword(user, password, user.PasswordHash))
        {
            result.IsSuccess = false;
            result.ErrorMessage = "密码错误";
            await _loginLogService.RecordLoginLogAsync(username, ipAddress, false, result.ErrorMessage);
            return result;
        }
 
        // 5. 登录成功,记录日志并返回结果
        result.IsSuccess = true;
        result.UserInfo = new UserInfoDto
        {
            Id = user.Id,
            Username = user.Username,
            Nickname = user.Nickname,
            Email = user.Email
        };
        var loginLog = await _loginLogService.RecordLoginLogAsync(username, ipAddress, true, "登录成功");
        result.LoginLog = new LoginLogDto
        {
            Id = loginLog.Id,
            LoginTime = loginLog.LoginTime,
            IpAddress = loginLog.IpAddress
        };
 
        return result;
    }
 
    public async Task<User> GetUserByUsernameAsync(string username)
    {
        return await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == username);
    }
}

5. 依赖注入服务配置#

ASP.NET MVC 5原生支持依赖注入,我们可以通过Autofac(第三方DI容器,功能更强大)或原生IDependencyResolver配置服务。

5.1 使用Autofac配置DI#

  1. 安装NuGet包:Autofac.Mvc5
  2. Global.asax中注册服务:
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
 
    // 配置Autofac容器
    var builder = new ContainerBuilder();
    // 注册所有控制器
    builder.RegisterControllers(typeof(MvcApplication).Assembly);
    // 注册EF上下文(每个请求实例化一次)
    builder.RegisterType<AppDbContext>().InstancePerRequest();
    // 注册业务服务
    builder.RegisterType<UserAuthService>().As<IUserAuthService>().InstancePerRequest();
    builder.RegisterType<LoginLogService>().As<ILoginLogService>().InstancePerRequest();
    // 构建容器并设置依赖解析器
    var container = builder.Build();
    DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}

6. 单元测试示例(最佳实践)#

使用xUnit+Moq编写单元测试,验证核心业务逻辑(无需依赖真实数据库):

public class UserAuthServiceTests
{
    [Fact]
    public async Task VerifyLoginAsync_UserNotFound_ReturnsFailure()
    {
        // Arrange
        var mockDbContext = new Mock<AppDbContext>();
        var mockLoginLogService = new Mock<ILoginLogService>();
        var userAuthService = new UserAuthService(mockDbContext.Object, mockLoginLogService.Object);
 
        // Act
        var result = await userAuthService.VerifyLoginAsync("test", "123456", "127.0.0.1");
 
        // Assert
        Assert.False(result.IsSuccess);
        Assert.Equal("用户名不存在", result.ErrorMessage);
        mockLoginLogService.Verify(s => s.RecordLoginLogAsync(It.IsAny<string>(), It.IsAny<string>(), false, It.IsAny<string>()), Times.Once);
    }
}

7. 常见问题与行业最佳实践#

常见问题#

  1. 密码加密误区:切勿使用MD5、SHA1等单向哈希(无盐),必须使用带随机盐的哈希算法(如BCrypt、Argon2)。
  2. 异步编程死锁:在业务层避免使用.Result/.Wait(),始终使用async/await保持异步上下文一致。
  3. 敏感数据暴露:禁止直接返回EF实体给前端,必须使用DTO过滤敏感字段(如PasswordHash)。

最佳实践#

  1. 依赖倒置优先:始终通过抽象接口编写业务逻辑,便于测试与扩展。
  2. 登录日志强制化:记录所有登录行为(包括失败),满足安全审计需求。
  3. 参数校验分层:业务层做核心参数校验,控制器层做格式校验,保证逻辑一致性。
  4. 错误信息封装:将系统异常转化为友好的业务错误信息,避免泄露系统细节。
  5. 暴力破解防护:可扩展登录失败次数限制(如5次错误后锁定15分钟),提升系统安全性。

8. 总结与下章预告#

本章总结#

我们完成了登录功能的核心服务层设计:

  • 抽象了IUserAuthServiceILoginLogService接口,实现了业务逻辑与数据访问的解耦
  • 编写了包含密码验证、日志记录的实现类,遵循异步编程与安全规范
  • 配置了依赖注入,实现了服务的自动管理
  • 给出了单元测试示例,保障业务逻辑的正确性

下章预告#

下一章我们将基于本章实现的服务层,完成登录控制器的编写Cookie身份认证的配置,实现从前端请求到后端验证的完整登录流程。


9. 参考资料#

  1. ASP.NET MVC 5 官方文档
  2. EF Core 异步操作文档
  3. 密码哈希最佳实践
  4. Autofac 官方文档
  5. xUnit 单元测试指南