【无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(5.4)—— 登录功能的实现:创建与登录用户相关的接口和实现类
登录功能是几乎所有Web系统的核心入口,它不仅关乎用户身份验证的安全性,更直接影响系统的可用性与可维护性。在ASP.NET MVC架构中,通过抽象接口+实现类的方式构建登录逻辑,是遵循依赖倒置原则、实现代码解耦与可扩展的最佳实践。
本章我们将从需求分析入手,逐步完成登录相关接口的抽象、实现类的编写、依赖注入配置,并结合密码加密、登录日志记录等核心逻辑,全方位打造一个安全、可维护的登录服务体系。
目录#
- 前置知识回顾与本章目标
- 登录功能需求与领域模型梳理
- 登录相关接口抽象设计
- 接口实现类核心逻辑编写
- 依赖注入服务配置
- 单元测试示例(最佳实践)
- 常见问题与行业最佳实践
- 总结与下章预告
- 参考资料
1. 前置知识回顾与本章目标#
前置知识#
- 熟悉ASP.NET MVC 5架构与EF Core数据访问
- 掌握C#异步编程(
async/await) - 了解依赖倒置原则与依赖注入(DI)概念
- 知晓密码哈希加密的基本原理
本章目标#
- 完成登录相关业务接口的抽象设计
- 实现包含密码验证、登录日志的核心业务逻辑
- 配置依赖注入实现服务解耦
- 掌握登录功能的安全与性能最佳实践
2. 登录功能需求与领域模型梳理#
核心需求#
- 用户名+密码的身份验证,返回登录结果(成功/失败+错误信息)
- 密码必须加密存储与验证,禁止明文传输/存储
- 记录登录行为日志(时间、IP、登录状态、备注)
- 支持异步操作,提升系统并发处理能力
领域模型定义#
首先定义核心实体类(基于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#
- 安装NuGet包:
Autofac.Mvc5 - 在
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. 常见问题与行业最佳实践#
常见问题#
- 密码加密误区:切勿使用MD5、SHA1等单向哈希(无盐),必须使用带随机盐的哈希算法(如BCrypt、Argon2)。
- 异步编程死锁:在业务层避免使用
.Result/.Wait(),始终使用async/await保持异步上下文一致。 - 敏感数据暴露:禁止直接返回EF实体给前端,必须使用DTO过滤敏感字段(如
PasswordHash)。
最佳实践#
- 依赖倒置优先:始终通过抽象接口编写业务逻辑,便于测试与扩展。
- 登录日志强制化:记录所有登录行为(包括失败),满足安全审计需求。
- 参数校验分层:业务层做核心参数校验,控制器层做格式校验,保证逻辑一致性。
- 错误信息封装:将系统异常转化为友好的业务错误信息,避免泄露系统细节。
- 暴力破解防护:可扩展登录失败次数限制(如5次错误后锁定15分钟),提升系统安全性。
8. 总结与下章预告#
本章总结#
我们完成了登录功能的核心服务层设计:
- 抽象了
IUserAuthService与ILoginLogService接口,实现了业务逻辑与数据访问的解耦 - 编写了包含密码验证、日志记录的实现类,遵循异步编程与安全规范
- 配置了依赖注入,实现了服务的自动管理
- 给出了单元测试示例,保障业务逻辑的正确性
下章预告#
下一章我们将基于本章实现的服务层,完成登录控制器的编写与Cookie身份认证的配置,实现从前端请求到后端验证的完整登录流程。