using Dpz.Core.Entity.Base.ExpressTreeQuery;
using Dpz.Core.Public.ViewModel.Request;
using Dpz.Core.Public.ViewModel.Response;
using Medallion.Threading;

namespace Dpz.Core.Service.RepositoryServiceImpl;

public class AccountLoginHistoryService(
    IRepository<AccountLoginHistory> repository,
    IHttpContextAccessor httpContextAccessor,
    IDistributedLockProvider distributedLockProvider,
    IConfiguration configuration
) : IAccountLoginHistoryService
{
    private static readonly HashSet<LoginResultStatus> FailStatuses =
    [
        LoginResultStatus.AccountOrPasswordError,
        LoginResultStatus.PinCodeError,
        LoginResultStatus.AccountLocked,
        LoginResultStatus.AccountDisabled,
        LoginResultStatus.PasskeyUserVerificationFailed,
    ];

    private async Task AddAsync(string account, LoginMethod method, LoginResultStatus status)
    {
        await using (await distributedLockProvider.AcquireLockAsync($"Login:Fail:{account}"))
        {
            var ipAddress = httpContextAccessor.HttpContext?.Request.GetIpAddress();
            var userAgent = httpContextAccessor.HttpContext?.Request.Headers.UserAgent;
            var sessionId = httpContextAccessor.HttpContext?.Request.Cookies["SessionId"];
            await repository.InsertAsync(
                new AccountLoginHistory
                {
                    Account = account,
                    IpAddress = ipAddress,
                    UserAgent = userAgent,
                    SessionId = sessionId,
                    Method = method,
                    Status = status,
                    CreateTime = DateTime.Now,
                    LastUpdateTime = DateTime.Now,
                }
            );
        }
    }

    public async Task LoginFailAsync(
        string account,
        LoginMethod method = LoginMethod.Password,
        LoginResultStatus status = LoginResultStatus.AccountOrPasswordError
    )
    {
        if (!FailStatuses.Contains(status))
        {
            throw new NotSupportedException();
        }
        await AddAsync(account, method, status);
    }

    public async Task PinCodeErrorAsync(string account, LoginMethod method = LoginMethod.Password)
    {
        await AddAsync(account, method, LoginResultStatus.PinCodeError);
    }

    public async Task AccountLockedAsync(string account, LoginMethod method = LoginMethod.Password)
    {
        await AddAsync(account, method, LoginResultStatus.AccountLocked);
    }

    public async Task AccountDisabledAsync(
        string account,
        LoginMethod method = LoginMethod.Password
    )
    {
        await AddAsync(account, method, LoginResultStatus.AccountDisabled);
    }

    public async Task SuccessAsync(string account, LoginMethod method = LoginMethod.Password)
    {
        await using (await distributedLockProvider.AcquireLockAsync($"Login:Success:{account}"))
        {
            var httpContext = httpContextAccessor.HttpContext;
            var ipAddress = httpContext?.Request.GetIpAddress();
            var userAgent = httpContext?.Request.Headers.UserAgent;
            var sessionId = httpContext?.Request.Cookies["SessionId"];

            if (!string.IsNullOrWhiteSpace(sessionId))
            {
                var window = TimeSpan.FromSeconds(60);
                var recent = await repository
                    .SearchFor(x =>
                        x.Account == account
                        && x.SessionId == sessionId
                        && x.Status == LoginResultStatus.Success
                        && x.CreateTime >= DateTime.Now.Add(-window)
                    )
                    .FirstOrDefaultAsync();
                if (recent != null)
                {
                    return;
                }
            }

            await repository.InsertAsync(
                new AccountLoginHistory
                {
                    Account = account,
                    IpAddress = ipAddress,
                    UserAgent = userAgent,
                    SessionId = sessionId,
                    Method = method,
                    Status = LoginResultStatus.Success,
                    CreateTime = DateTime.Now,
                    LastUpdateTime = DateTime.Now,
                }
            );
        }
    }

    public async Task<bool> LockedAsync(string account)
    {
        var endTime = DateTime.Now;
        var lockPeriod = configuration.GetValue("AccountLockPeriodHours", 12);
        var startTime = endTime.AddHours(-lockPeriod);
        var count = await repository
            .SearchFor(x =>
                x.Account == account
                && x.Status == LoginResultStatus.AccountOrPasswordError
                && x.CreateTime >= startTime
                && x.CreateTime < endTime
            )
            .CountAsync();
        var maxCount = configuration.GetValue("AccountLoginFailTryMaxCount", 3);
        return count >= maxCount;
    }

    public async Task<IPagedList<AccountLoginHistoryResponse>> GetPageAsync(
        AccountLoginHistoryRequest request
    )
    {
        var filter = GenerateConditionFilter.GenerateConditionExpressionTree<
            AccountLoginHistory,
            AccountLoginHistoryRequest
        >(request);
        return await repository
            .SearchFor(filter)
            .OrderByDescending(x => x.CreateTime)
            .ToPagedListAsync<AccountLoginHistory, AccountLoginHistoryResponse>(
                request.PageIndex,
                request.PageSize
            );
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 用户账户登录历史服务 的实现类,用于记录和管理用户登录行为,包括成功登录、失败登录以及账户锁定逻辑。

核心功能模块

1. 依赖注入构造函数

public class AccountLoginHistoryService(
    IRepository<AccountLoginHistory> repository,
    IHttpContextAccessor httpContextAccessor,
    IDistributedLockProvider distributedLockProvider,
    IConfiguration configuration
)

使用主构造函数注入了四个依赖:

  • repository: 数据仓储,用于数据库操作
  • httpContextAccessor: 访问HTTP上下文,获取请求信息
  • distributedLockProvider: 分布式锁提供者(防止并发问题)
  • configuration: 配置信息访问

2. 失败状态定义

private static readonly HashSet<LoginResultStatus> FailStatuses = [...]

定义了所有被认为是"登录失败"的状态集合,用于验证。

3. 核心私有方法 AddAsync

记录登录历史的通用方法:

  • 使用分布式锁 $"Login:Fail:{account}" 防止并发写入
  • 从HTTP上下文提取:IP地址、UserAgent、SessionId
  • 插入登录历史记录到数据库

4. 公共方法

LoginFailAsync - 记录登录失败

  • 验证状态是否在失败状态集合中
  • 调用 AddAsync 记录失败信息

PinCodeErrorAsync - PIN码错误

AccountLockedAsync - 账户已锁定

AccountDisabledAsync - 账户已禁用

以上三个方法都是特定场景的快捷方法。

SuccessAsync - 记录成功登录 ⭐

关键逻辑

var window = TimeSpan.FromSeconds(60);
var recent = await repository.SearchFor(x =>
    x.Account == account
    && x.SessionId == sessionId
    && x.Status == LoginResultStatus.Success
    && x.CreateTime >= DateTime.Now.Add(-window)
).FirstOrDefaultAsync();
  • 使用分布式锁 $"Login:Success:{account}"
  • 去重机制:如果同一SessionId在60秒内已有成功记录,则不重复插入
  • 防止短时间内重复记录同一会话的成功登录

LockedAsync - 判断账户是否应被锁定 🔒

var lockPeriod = configuration.GetValue("AccountLockPeriodHours", 12);
var maxCount = configuration.GetValue("AccountLoginFailTryMaxCount", 3);

锁定逻辑

  • 统计指定时间窗口内(默认12小时)的失败登录次数
  • 如果失败次数达到阈值(默认3次),返回 true(应锁定)

GetPageAsync - 分页查询登录历史

  • 使用 GenerateConditionFilter 动态生成查询条件
  • 按创建时间降序排列
  • 返回分页数据并映射到响应模型

设计亮点

  1. 分布式锁: 使用 Medallion.Threading 防止并发问题
  2. 防重复记录: 60秒窗口内同SessionId不重复记录成功登录
  3. 账户保护: 基于配置的失败次数锁定机制
  4. 信息完整: 记录IP、UserAgent、SessionId等审计信息
  5. 灵活配置: 锁定周期和失败次数阈值可配置

安全考虑

  • 使用分布式锁避免竞态条件
  • 限制失败登录次数防止暴力破解
  • 完整的审计日志便于安全分析
评论加载中...