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动态生成查询条件 - 按创建时间降序排列
- 返回分页数据并映射到响应模型
设计亮点
- 分布式锁: 使用 Medallion.Threading 防止并发问题
- 防重复记录: 60秒窗口内同SessionId不重复记录成功登录
- 账户保护: 基于配置的失败次数锁定机制
- 信息完整: 记录IP、UserAgent、SessionId等审计信息
- 灵活配置: 锁定周期和失败次数阈值可配置
安全考虑
- 使用分布式锁避免竞态条件
- 限制失败登录次数防止暴力破解
- 完整的审计日志便于安全分析
AI 正在分析代码…
评论加载中...