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
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("认证失败,请稍后重试");
|
|
}
|
|
}
|
|
}
|