2 changed files with 262 additions and 0 deletions
@ -0,0 +1,20 @@ |
|||
using MediatR; |
|||
using CellularManagement.Domain.Common; |
|||
|
|||
namespace CellularManagement.Application.Features.Auth.Commands.Logout; |
|||
|
|||
/// <summary>
|
|||
/// 登出命令
|
|||
/// </summary>
|
|||
public class LogoutCommand : IRequest<OperationResult<bool>> |
|||
{ |
|||
/// <summary>
|
|||
/// 访问令牌
|
|||
/// </summary>
|
|||
public string AccessToken { get; set; } = string.Empty; |
|||
|
|||
/// <summary>
|
|||
/// 刷新令牌
|
|||
/// </summary>
|
|||
public string RefreshToken { get; set; } = string.Empty; |
|||
} |
@ -0,0 +1,242 @@ |
|||
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; |
|||
|
|||
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 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) |
|||
{ |
|||
_jwtProvider = jwtProvider; |
|||
_logger = logger; |
|||
_loginLogRepository = loginLogRepository; |
|||
_unitOfWork = unitOfWork; |
|||
} |
|||
|
|||
/// <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 loginLog = new LoginLog |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
UserId = userId, |
|||
LoginTime = DateTime.UtcNow, |
|||
IsSuccess = true |
|||
}; |
|||
|
|||
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); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
LogTokenProcessingError(_logger, tokenType, "处理令牌时发生异常", ex); |
|||
throw; |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue