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.
260 lines
11 KiB
260 lines
11 KiB
using MediatR;
|
|
using Microsoft.Extensions.Logging;
|
|
using CellularManagement.Domain.Common;
|
|
using CellularManagement.Domain.Services;
|
|
using CellularManagement.Domain.Repositories;
|
|
using CellularManagement.Domain.Entities;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Security.Claims;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using CellularManagement.Domain.Entities.Logging;
|
|
using Microsoft.AspNetCore.Http;
|
|
using CellularManagement.Domain.Repositories.Base;
|
|
using CellularManagement.Domain.Repositories.Identity;
|
|
|
|
namespace CellularManagement.Application.Features.Auth.Commands.Logout;
|
|
|
|
/// <summary>
|
|
/// 登出命令处理器
|
|
/// </summary>
|
|
public class LogoutCommandHandler : IRequestHandler<LogoutCommand, OperationResult<bool>>
|
|
{
|
|
private readonly IJwtProvider _jwtProvider;
|
|
private readonly ILogger<LogoutCommandHandler> _logger;
|
|
private readonly ILoginLogRepository _loginLogRepository;
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
private static readonly Action<ILogger, string, Exception?> LogTokenValidationError =
|
|
LoggerMessage.Define<string>(LogLevel.Warning, new EventId(1, "TokenValidationError"), "令牌验证错误: {Message}");
|
|
private static readonly Action<ILogger, string, string, Exception?> LogTokenProcessingError =
|
|
LoggerMessage.Define<string, string>(LogLevel.Error, new EventId(2, "TokenProcessingError"), "处理令牌 {TokenType} 时发生错误: {Message}");
|
|
|
|
/// <summary>
|
|
/// 初始化处理器
|
|
/// </summary>
|
|
public LogoutCommandHandler(
|
|
IJwtProvider jwtProvider,
|
|
ILogger<LogoutCommandHandler> logger,
|
|
ILoginLogRepository loginLogRepository,
|
|
IUnitOfWork unitOfWork,
|
|
IHttpContextAccessor httpContextAccessor)
|
|
{
|
|
_jwtProvider = jwtProvider;
|
|
_logger = logger;
|
|
_loginLogRepository = loginLogRepository;
|
|
_unitOfWork = unitOfWork;
|
|
_httpContextAccessor = httpContextAccessor;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理登出请求
|
|
/// </summary>
|
|
public async Task<OperationResult<bool>> Handle(LogoutCommand request, CancellationToken cancellationToken)
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
var correlationId = Guid.NewGuid().ToString();
|
|
_logger.LogInformation("[{CorrelationId}] 开始处理登出请求", correlationId);
|
|
|
|
try
|
|
{
|
|
// 快速验证令牌存在性
|
|
if (string.IsNullOrEmpty(request.AccessToken) || string.IsNullOrEmpty(request.RefreshToken))
|
|
{
|
|
_logger.LogWarning("[{CorrelationId}] 令牌为空", correlationId);
|
|
return OperationResult<bool>.CreateFailure("无效的请求");
|
|
}
|
|
|
|
_logger.LogDebug("[{CorrelationId}] 开始验证令牌", correlationId);
|
|
string? userId = null;
|
|
bool isAccessTokenValid = false;
|
|
bool isRefreshTokenValid = false;
|
|
var tokenValidationSw = Stopwatch.StartNew();
|
|
|
|
try
|
|
{
|
|
// 验证访问令牌
|
|
_logger.LogDebug("[{CorrelationId}] 开始验证访问令牌", correlationId);
|
|
var accessTokenClaims = _jwtProvider.GetClaimsFromToken(request.AccessToken);
|
|
if (accessTokenClaims.Any())
|
|
{
|
|
var tokenType = accessTokenClaims.FirstOrDefault(c => c.Type == "token_type")?.Value;
|
|
_logger.LogDebug("[{CorrelationId}] 访问令牌类型: {TokenType}", correlationId, tokenType);
|
|
|
|
if (tokenType == "access_token")
|
|
{
|
|
userId = accessTokenClaims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
|
isAccessTokenValid = !string.IsNullOrEmpty(userId);
|
|
_logger.LogDebug("[{CorrelationId}] 访问令牌验证结果: {IsValid}, 用户ID: {UserId}",
|
|
correlationId, isAccessTokenValid, userId);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("[{CorrelationId}] 访问令牌类型不匹配: {TokenType}", correlationId, tokenType);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogTokenValidationError(_logger, "访问令牌验证失败", ex);
|
|
}
|
|
|
|
try
|
|
{
|
|
// 验证刷新令牌
|
|
_logger.LogDebug("[{CorrelationId}] 开始验证刷新令牌", correlationId);
|
|
var refreshTokenClaims = _jwtProvider.GetClaimsFromToken(request.RefreshToken);
|
|
if (refreshTokenClaims.Any())
|
|
{
|
|
var tokenType = refreshTokenClaims.FirstOrDefault(c => c.Type == "token_type")?.Value;
|
|
_logger.LogDebug("[{CorrelationId}] 刷新令牌类型: {TokenType}", correlationId, tokenType);
|
|
|
|
if (tokenType == "refresh_token")
|
|
{
|
|
var refreshTokenUserId = refreshTokenClaims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
|
isRefreshTokenValid = !string.IsNullOrEmpty(refreshTokenUserId) &&
|
|
(string.IsNullOrEmpty(userId) || userId == refreshTokenUserId);
|
|
|
|
if (isRefreshTokenValid && string.IsNullOrEmpty(userId))
|
|
{
|
|
userId = refreshTokenUserId;
|
|
}
|
|
_logger.LogDebug("[{CorrelationId}] 刷新令牌验证结果: {IsValid}, 用户ID: {UserId}",
|
|
correlationId, isRefreshTokenValid, refreshTokenUserId);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("[{CorrelationId}] 刷新令牌类型不匹配: {TokenType}", correlationId, tokenType);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogTokenValidationError(_logger, "刷新令牌验证失败", ex);
|
|
}
|
|
|
|
tokenValidationSw.Stop();
|
|
_logger.LogInformation("[{CorrelationId}] 令牌验证完成,耗时: {ElapsedMs}ms",
|
|
correlationId, tokenValidationSw.ElapsedMilliseconds);
|
|
|
|
// 如果两个令牌都无效,直接返回成功
|
|
if (!isAccessTokenValid && !isRefreshTokenValid)
|
|
{
|
|
_logger.LogInformation("[{CorrelationId}] 所有令牌无效,直接返回成功", correlationId);
|
|
return OperationResult<bool>.CreateSuccess(true);
|
|
}
|
|
|
|
// 记录登出日志
|
|
if (!string.IsNullOrEmpty(userId))
|
|
{
|
|
_logger.LogDebug("[{CorrelationId}] 开始记录登出日志,用户ID: {UserId}", correlationId, userId);
|
|
var logSw = Stopwatch.StartNew();
|
|
await RecordLogoutLogAsync(userId, correlationId, cancellationToken);
|
|
logSw.Stop();
|
|
_logger.LogInformation("[{CorrelationId}] 登出日志记录完成,耗时: {ElapsedMs}ms",
|
|
correlationId, logSw.ElapsedMilliseconds);
|
|
}
|
|
|
|
// 异步处理令牌撤销和黑名单
|
|
_ = Task.Run(async () =>
|
|
{
|
|
var tokenProcessingSw = Stopwatch.StartNew();
|
|
try
|
|
{
|
|
if (isAccessTokenValid)
|
|
{
|
|
_logger.LogDebug("[{CorrelationId}] 开始处理访问令牌撤销和黑名单", correlationId);
|
|
await ProcessTokenAsync(request.AccessToken, "访问令牌", correlationId);
|
|
}
|
|
|
|
if (isRefreshTokenValid)
|
|
{
|
|
_logger.LogDebug("[{CorrelationId}] 开始处理刷新令牌撤销和黑名单", correlationId);
|
|
await ProcessTokenAsync(request.RefreshToken, "刷新令牌", correlationId);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogTokenProcessingError(_logger, "令牌处理", "处理令牌撤销和黑名单时发生异常", ex);
|
|
}
|
|
finally
|
|
{
|
|
tokenProcessingSw.Stop();
|
|
_logger.LogInformation("[{CorrelationId}] 令牌处理完成,耗时: {ElapsedMs}ms",
|
|
correlationId, tokenProcessingSw.ElapsedMilliseconds);
|
|
}
|
|
});
|
|
|
|
sw.Stop();
|
|
_logger.LogInformation("[{CorrelationId}] 登出请求处理完成,总耗时: {ElapsedMs}ms",
|
|
correlationId, sw.ElapsedMilliseconds);
|
|
return OperationResult<bool>.CreateSuccess(true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
sw.Stop();
|
|
_logger.LogError(ex, "[{CorrelationId}] 处理登出请求时发生异常,耗时: {ElapsedMs}ms",
|
|
correlationId, sw.ElapsedMilliseconds);
|
|
return OperationResult<bool>.CreateFailure("系统错误");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 记录登出日志
|
|
/// </summary>
|
|
private async Task RecordLogoutLogAsync(string userId, string correlationId, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var httpContext = _httpContextAccessor.HttpContext;
|
|
var ipAddress = httpContext?.Connection.RemoteIpAddress?.ToString() ?? "Unknown";
|
|
var userAgent = httpContext?.Request.Headers["User-Agent"].ToString() ?? "Unknown";
|
|
|
|
// 解析设备信息
|
|
var parser = UAParser.Parser.GetDefault();
|
|
var clientInfo = parser.Parse(userAgent);
|
|
|
|
var loginLog = LoginLog.Create(
|
|
userId: userId,
|
|
ipAddress: ipAddress,
|
|
userAgent: userAgent,
|
|
isSuccess: true,
|
|
loginType: "Logout",
|
|
loginSource: "Web",
|
|
browser: clientInfo.UA.ToString(),
|
|
operatingSystem: clientInfo.OS.ToString());
|
|
|
|
await _loginLogRepository.AddAsync(loginLog, cancellationToken);
|
|
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
|
_logger.LogDebug("[{CorrelationId}] 登出日志记录成功", correlationId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[{CorrelationId}] 记录登出日志时发生异常", correlationId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理令牌撤销和黑名单
|
|
/// </summary>
|
|
private async Task ProcessTokenAsync(string token, string tokenType, string correlationId)
|
|
{
|
|
try
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
_jwtProvider.RevokeToken(token);
|
|
_jwtProvider.AddToBlacklist(token);
|
|
sw.Stop();
|
|
_logger.LogDebug("[{CorrelationId}] {TokenType}处理完成,耗时: {ElapsedMs}ms",
|
|
correlationId, tokenType, sw.ElapsedMilliseconds);
|
|
await Task.CompletedTask;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogTokenProcessingError(_logger, tokenType, "处理令牌时发生异常", ex);
|
|
throw;
|
|
}
|
|
}
|
|
}
|