You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

322 lines
13 KiB

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using MediatR;
using X1.Domain.Entities;
using X1.Domain.Repositories;
using X1.Domain.Services;
using System.Threading.Tasks;
using System.Threading;
using X1.Domain.Common;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using X1.Application.Features.Auth.Models;
using X1.Domain.Entities.Logging;
using X1.Domain.Repositories.Identity;
using X1.Domain.Repositories.Base;
using X1.Domain.Options;
using Microsoft.Extensions.Options;
namespace X1.Application.Features.Auth.Commands;
/// <summary>
/// 登录命令处理器基类
/// </summary>
public abstract class BaseLoginCommandHandler<TCommand, TResponse> : IRequestHandler<TCommand, OperationResult<TResponse>>
where TCommand : IRequest<OperationResult<TResponse>>
where TResponse : class
{
protected readonly UserManager<AppUser> _userManager;
protected readonly IJwtProvider _jwtProvider;
protected readonly ILogger _logger;
protected readonly IUserRoleRepository _userRoleRepository;
protected readonly IRolePermissionRepository _rolePermissionRepository;
protected readonly IUnitOfWork _unitOfWork;
protected readonly ILoginLogRepository _loginLogRepository;
protected readonly IHttpContextAccessor _httpContextAccessor;
protected readonly ISessionManagementService _sessionManagementService;
protected readonly JwtOptions _jwtOptions;
private const int MaxRetryAttempts = 3;
/// <summary>
/// 初始化处理器
/// </summary>
protected BaseLoginCommandHandler(
UserManager<AppUser> userManager,
IJwtProvider jwtProvider,
ILogger logger,
IUserRoleRepository userRoleRepository,
IRolePermissionRepository rolePermissionRepository,
IUnitOfWork unitOfWork,
ILoginLogRepository loginLogRepository,
IHttpContextAccessor httpContextAccessor,
ISessionManagementService sessionManagementService,
IOptions<JwtOptions> jwtOptions)
{
_userManager = userManager;
_jwtProvider = jwtProvider;
_logger = logger;
_userRoleRepository = userRoleRepository;
_rolePermissionRepository = rolePermissionRepository;
_unitOfWork = unitOfWork;
_loginLogRepository = loginLogRepository;
_httpContextAccessor = httpContextAccessor;
_sessionManagementService = sessionManagementService;
_jwtOptions = jwtOptions.Value;
}
/// <summary>
/// 处理登录请求
/// </summary>
public abstract Task<OperationResult<TResponse>> Handle(TCommand request, CancellationToken cancellationToken);
/// <summary>
/// 查找用户
/// </summary>
protected abstract Task<AppUser?> FindUserAsync(TCommand request);
/// <summary>
/// 获取用户标识
/// </summary>
protected abstract string GetUserIdentifier(TCommand request);
/// <summary>
/// 获取密码
/// </summary>
protected abstract string GetPassword(TCommand request);
/// <summary>
/// 验证凭据
/// </summary>
protected virtual async Task<bool> ValidateCredentialsAsync(TCommand request, AppUser user)
{
return await _userManager.CheckPasswordAsync(user, GetPassword(request));
}
/// <summary>
/// 处理登录逻辑
/// </summary>
protected async Task<OperationResult<TResponse>> HandleLoginAsync(TCommand request, CancellationToken cancellationToken)
{
try
{
var httpContext = _httpContextAccessor.HttpContext;
var ipAddress = httpContext?.Connection.RemoteIpAddress?.ToString() ?? "Unknown";
var userAgent = httpContext?.Request.Headers["User-Agent"].ToString() ?? "Unknown";
// 检查IP是否被限制
if (await _loginLogRepository.IsIpRestrictedAsync(ipAddress, cancellationToken))
{
_logger.LogWarning("IP {IpAddress} 已被限制登录", ipAddress);
return OperationResult<TResponse>.CreateFailure("登录尝试次数过多,请稍后再试");
}
// 解析设备信息
var parser = UAParser.Parser.GetDefault();
var clientInfo = parser.Parse(userAgent);
// 查找用户
var user = await FindUserAsync(request);
var userIdentifier = GetUserIdentifier(request);
// 创建登录日志
var loginLog = LoginLog.Create(
userId: user?.Id ?? "Unknown",
ipAddress: ipAddress,
userAgent: userAgent,
isSuccess: false,
loginType: "Password",
loginSource: "Web",
browser: clientInfo.UA.ToString(),
operatingSystem: clientInfo.OS.ToString());
if (user == null)
{
loginLog.UpdateFailureReason("用户不存在");
await _loginLogRepository.RecordLoginAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserIdentifier} 不存在", userIdentifier);
return OperationResult<TResponse>.CreateFailure("用户不存在");
}
// 检查用户是否已删除
if (user.IsDeleted)
{
loginLog.UpdateFailureReason("用户已被删除");
await _loginLogRepository.RecordLoginAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserIdentifier} 已被删除", userIdentifier);
return OperationResult<TResponse>.CreateFailure("用户已被删除");
}
// 检查用户是否已禁用
if (!user.IsActive)
{
loginLog.UpdateFailureReason("用户已被禁用");
await _loginLogRepository.RecordLoginAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserIdentifier} 已被禁用", userIdentifier);
return OperationResult<TResponse>.CreateFailure("用户已被禁用");
}
// 验证凭据
var isValidCredentials = await ValidateCredentialsAsync(request, user);
if (!isValidCredentials)
{
loginLog.UpdateFailureReason("验证失败");
await _loginLogRepository.RecordLoginAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserIdentifier} 验证失败", userIdentifier);
return OperationResult<TResponse>.CreateFailure("验证失败");
}
// 更新最后登录时间(带并发控制)
var retryCount = 0;
var updateSuccess = false;
while (!updateSuccess && retryCount < MaxRetryAttempts)
{
try
{
await _unitOfWork.ExecuteTransactionAsync(async () =>
{
user.LastLoginTime = DateTime.UtcNow;
var result = await _userManager.UpdateAsync(user);
if (!result.Succeeded)
{
var errors = result.Errors.Select(e => e.Description).ToList();
_logger.LogWarning("更新用户最后登录时间失败: {Errors}", string.Join(", ", errors));
throw new InvalidOperationException(string.Join(", ", errors));
}
}, cancellationToken: cancellationToken);
updateSuccess = true;
}
catch (DbUpdateConcurrencyException)
{
retryCount++;
_logger.LogWarning("用户 {UserIdentifier} 更新时发生并发冲突,重试次数: {RetryCount}",
userIdentifier, retryCount);
if (retryCount >= MaxRetryAttempts)
{
_logger.LogError("用户 {UserIdentifier} 更新失败,超过最大重试次数", userIdentifier);
return OperationResult<TResponse>.CreateFailure("系统繁忙,请稍后重试");
}
// 重新获取最新数据
user = await _userManager.FindByIdAsync(user.Id);
if (user == null)
{
return OperationResult<TResponse>.CreateFailure("用户不存在");
}
}
}
// 获取用户角色
var roles = await _userRoleRepository.GetUserRolesAsync(user.Id, cancellationToken);
// 创建用户声明
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Name, user.UserName!),
new(ClaimTypes.Email, user.Email!)
};
// 添加最后登录时间声明(如果存在)
if (user.LastLoginTime.HasValue)
{
claims.Add(new Claim("LastLoginTime", user.LastLoginTime.Value.ToUniversalTime().ToString("o")));
}
// 添加角色声明
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
// 获取所有角色的权限
var permissionCodes = new HashSet<string>();
if (roles.Any())
{
var allRolePermissions = await _rolePermissionRepository.GetRolePermissionsByRolesAsync(roles, cancellationToken);
foreach (var rolePermission in allRolePermissions)
{
if (rolePermission.Permission != null)
{
permissionCodes.Add(rolePermission.Permission.Code);
}
}
}
// 生成会话ID
var sessionId = Guid.NewGuid().ToString();
// 添加会话ID到Claims(在生成令牌之前)- 使用ClaimTypes.UserData
claims.Add(new Claim(ClaimTypes.UserData, sessionId));
// 生成访问令牌
var accessToken = _jwtProvider.GenerateAccessToken(claims);
// 生成刷新令牌
var refreshToken = _jwtProvider.GenerateRefreshToken(claims);
// 获取令牌过期时间
var expiresAt = _jwtProvider.GetTokenExpiration(accessToken);
// 创建设备信息
var deviceInfo = $"{clientInfo.OS.Family} {clientInfo.OS.Major}.{clientInfo.OS.Minor} - {clientInfo.UA.Family} {clientInfo.UA.Major}.{clientInfo.UA.Minor}";
// 创建用户会话(踢出其他登录)
var sessionExpiry = TimeSpan.FromMinutes(_jwtOptions.ExpiryMinutes);
var sessionCreated = await _sessionManagementService.CreateSessionAsync(
user.Id,
sessionId,
accessToken,
deviceInfo,
sessionExpiry);
if (!sessionCreated)
{
_logger.LogError("创建用户会话失败: {UserId}", user.Id);
return OperationResult<TResponse>.CreateFailure("创建会话失败,请稍后重试");
}
// 创建用户信息
var userInfo = new UserInfo(
user.Id,
user.UserName!,
user.RealName,
user.Email!,
user.PhoneNumber,
roles.ToList().AsReadOnly(),
permissionCodes.ToList().AsReadOnly());
// 记录成功的登录日志
loginLog = LoginLog.Create(
userId: user.Id,
ipAddress: ipAddress,
userAgent: userAgent,
isSuccess: true,
loginType: "Password",
loginSource: "Web",
browser: clientInfo.UA.ToString(),
operatingSystem: clientInfo.OS.ToString());
await _loginLogRepository.RecordLoginAsync(loginLog, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
_logger.LogInformation("用户 {UserIdentifier} 认证成功", userIdentifier);
// 返回认证结果
var response = (TResponse)Activator.CreateInstance(
typeof(TResponse),
accessToken,
refreshToken,
expiresAt,
userInfo)!;
return OperationResult<TResponse>.CreateSuccess(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "用户认证失败");
return OperationResult<TResponse>.CreateFailure("认证失败,请稍后重试");
}
}
}