34 changed files with 1042 additions and 381 deletions
@ -1,28 +0,0 @@ |
|||
using System; |
|||
using CellularManagement.Application.Features.Auth.Common; |
|||
|
|||
namespace CellularManagement.Application.Features.Auth.Commands.AuthenticateUser; |
|||
|
|||
/// <summary>
|
|||
/// 用户认证响应
|
|||
/// </summary>
|
|||
public sealed record AuthenticateUserResponse( |
|||
/// <summary>
|
|||
/// 访问令牌
|
|||
/// </summary>
|
|||
string AccessToken, |
|||
|
|||
/// <summary>
|
|||
/// 刷新令牌
|
|||
/// </summary>
|
|||
string RefreshToken, |
|||
|
|||
/// <summary>
|
|||
/// 令牌过期时间
|
|||
/// </summary>
|
|||
DateTime ExpiresAt, |
|||
|
|||
/// <summary>
|
|||
/// 用户信息
|
|||
/// </summary>
|
|||
UserInfo User); |
@ -0,0 +1,283 @@ |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Logging; |
|||
using MediatR; |
|||
using CellularManagement.Domain.Entities; |
|||
using CellularManagement.Domain.Repositories; |
|||
using CellularManagement.Domain.Services; |
|||
using System.Threading.Tasks; |
|||
using System.Threading; |
|||
using CellularManagement.Domain.Common; |
|||
using Microsoft.AspNetCore.Http; |
|||
using System.Security.Claims; |
|||
using System; |
|||
using System.Linq; |
|||
using System.Collections.Generic; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using CellularManagement.Application.Features.Auth.Models; |
|||
|
|||
namespace CellularManagement.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; |
|||
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) |
|||
{ |
|||
_userManager = userManager; |
|||
_jwtProvider = jwtProvider; |
|||
_logger = logger; |
|||
_userRoleRepository = userRoleRepository; |
|||
_rolePermissionRepository = rolePermissionRepository; |
|||
_unitOfWork = unitOfWork; |
|||
_loginLogRepository = loginLogRepository; |
|||
_httpContextAccessor = httpContextAccessor; |
|||
} |
|||
|
|||
/// <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 = new LoginLog |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
UserId = user?.Id ?? "Unknown", |
|||
LoginTime = DateTime.UtcNow, |
|||
IpAddress = ipAddress, |
|||
UserAgent = userAgent, |
|||
Browser = clientInfo.UA.ToString(), |
|||
OperatingSystem = clientInfo.OS.ToString() |
|||
}; |
|||
|
|||
if (user == null) |
|||
{ |
|||
loginLog.IsSuccess = false; |
|||
loginLog.FailureReason = "用户不存在"; |
|||
await _loginLogRepository.AddAsync(loginLog, cancellationToken); |
|||
_logger.LogWarning("用户 {UserIdentifier} 不存在", userIdentifier); |
|||
return OperationResult<TResponse>.CreateFailure("账号或密码错误"); |
|||
} |
|||
|
|||
// 检查用户是否已删除
|
|||
if (user.IsDeleted) |
|||
{ |
|||
loginLog.IsSuccess = false; |
|||
loginLog.FailureReason = "用户已被删除"; |
|||
await _loginLogRepository.AddAsync(loginLog, cancellationToken); |
|||
_logger.LogWarning("用户 {UserIdentifier} 已被删除", userIdentifier); |
|||
return OperationResult<TResponse>.CreateFailure("用户已被删除"); |
|||
} |
|||
|
|||
// 检查用户是否已禁用
|
|||
if (!user.IsActive) |
|||
{ |
|||
loginLog.IsSuccess = false; |
|||
loginLog.FailureReason = "用户已被禁用"; |
|||
await _loginLogRepository.AddAsync(loginLog, cancellationToken); |
|||
_logger.LogWarning("用户 {UserIdentifier} 已被禁用", userIdentifier); |
|||
return OperationResult<TResponse>.CreateFailure("用户已被禁用"); |
|||
} |
|||
|
|||
// 验证凭据
|
|||
var isValidCredentials = await ValidateCredentialsAsync(request, user); |
|||
if (!isValidCredentials) |
|||
{ |
|||
loginLog.IsSuccess = false; |
|||
loginLog.FailureReason = "验证失败"; |
|||
await _loginLogRepository.AddAsync(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 permissions = new Dictionary<string, bool>(); |
|||
foreach (var role in roles) |
|||
{ |
|||
var rolePermissions = await _rolePermissionRepository.GetRolePermissionsWithDetailsAsync(role, cancellationToken); |
|||
foreach (var rolePermission in rolePermissions) |
|||
{ |
|||
if (!permissions.ContainsKey(rolePermission.Permission.Code)) |
|||
{ |
|||
permissions[rolePermission.Permission.Code] = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 生成访问令牌
|
|||
var accessToken = _jwtProvider.GenerateAccessToken(claims); |
|||
|
|||
// 生成刷新令牌
|
|||
var refreshToken = _jwtProvider.GenerateRefreshToken(claims); |
|||
|
|||
// 获取令牌过期时间
|
|||
var expiresAt = _jwtProvider.GetTokenExpiration(accessToken); |
|||
|
|||
// 创建用户信息
|
|||
var userInfo = new UserInfo( |
|||
user.Id, |
|||
user.UserName!, |
|||
user.RealName, |
|||
user.Email!, |
|||
user.PhoneNumber, |
|||
roles.ToList().AsReadOnly(), |
|||
permissions.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); |
|||
|
|||
// 记录成功的登录日志
|
|||
loginLog.IsSuccess = true; |
|||
await _loginLogRepository.AddAsync(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("认证失败,请稍后重试"); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,19 @@ |
|||
using CellularManagement.Domain.Common; |
|||
using MediatR; |
|||
using CellularManagement.Application.Features.Auth.Models; |
|||
|
|||
namespace CellularManagement.Application.Features.Auth.Commands.EmailLogin; |
|||
|
|||
/// <summary>
|
|||
/// 邮箱登录命令
|
|||
/// </summary>
|
|||
public sealed record EmailLoginCommand( |
|||
/// <summary>
|
|||
/// 邮箱
|
|||
/// </summary>
|
|||
string Email, |
|||
|
|||
/// <summary>
|
|||
/// 验证码
|
|||
/// </summary>
|
|||
string VerificationCode) : IRequest<OperationResult<EmailLoginResponse>>; |
@ -0,0 +1,82 @@ |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.Logging; |
|||
using MediatR; |
|||
using CellularManagement.Domain.Entities; |
|||
using CellularManagement.Domain.Repositories; |
|||
using CellularManagement.Domain.Services; |
|||
using System.Threading.Tasks; |
|||
using System.Threading; |
|||
using CellularManagement.Domain.Common; |
|||
using Microsoft.AspNetCore.Http; |
|||
using CellularManagement.Application.Features.Auth.Models; |
|||
|
|||
namespace CellularManagement.Application.Features.Auth.Commands.EmailLogin; |
|||
|
|||
/// <summary>
|
|||
/// 邮箱登录命令处理器
|
|||
/// </summary>
|
|||
public sealed class EmailLoginCommandHandler : BaseLoginCommandHandler<EmailLoginCommand, EmailLoginResponse> |
|||
{ |
|||
private readonly IEmailVerificationService _emailVerificationService; |
|||
|
|||
/// <summary>
|
|||
/// 初始化处理器
|
|||
/// </summary>
|
|||
public EmailLoginCommandHandler( |
|||
UserManager<AppUser> userManager, |
|||
IJwtProvider jwtProvider, |
|||
ILogger<EmailLoginCommandHandler> logger, |
|||
IUserRoleRepository userRoleRepository, |
|||
IRolePermissionRepository rolePermissionRepository, |
|||
IUnitOfWork unitOfWork, |
|||
ILoginLogRepository loginLogRepository, |
|||
IHttpContextAccessor httpContextAccessor, |
|||
IEmailVerificationService emailVerificationService) |
|||
: base(userManager, jwtProvider, logger, userRoleRepository, rolePermissionRepository, unitOfWork, loginLogRepository, httpContextAccessor) |
|||
{ |
|||
_emailVerificationService = emailVerificationService; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 处理认证请求
|
|||
/// </summary>
|
|||
public override Task<OperationResult<EmailLoginResponse>> Handle( |
|||
EmailLoginCommand request, |
|||
CancellationToken cancellationToken) |
|||
{ |
|||
return HandleLoginAsync(request, cancellationToken); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 查找用户
|
|||
/// </summary>
|
|||
protected override Task<AppUser?> FindUserAsync(EmailLoginCommand request) |
|||
{ |
|||
return _userManager.FindByEmailAsync(request.Email); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取用户标识
|
|||
/// </summary>
|
|||
protected override string GetUserIdentifier(EmailLoginCommand request) |
|||
{ |
|||
return request.Email; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取密码
|
|||
/// </summary>
|
|||
protected override string GetPassword(EmailLoginCommand request) |
|||
{ |
|||
return request.VerificationCode; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 验证凭据
|
|||
/// </summary>
|
|||
protected override async Task<bool> ValidateCredentialsAsync(EmailLoginCommand request, AppUser user) |
|||
{ |
|||
// 验证邮箱验证码
|
|||
return await _emailVerificationService.VerifyCodeAsync(request.Email, request.VerificationCode); |
|||
} |
|||
} |
@ -0,0 +1,29 @@ |
|||
using FluentValidation; |
|||
using CellularManagement.Application.Common.Extensions; |
|||
|
|||
namespace CellularManagement.Application.Features.Auth.Commands.EmailLogin; |
|||
|
|||
/// <summary>
|
|||
/// 邮箱登录命令验证器
|
|||
/// </summary>
|
|||
public sealed class EmailLoginCommandValidator : AbstractValidator<EmailLoginCommand> |
|||
{ |
|||
/// <summary>
|
|||
/// 初始化验证器
|
|||
/// </summary>
|
|||
public EmailLoginCommandValidator() |
|||
{ |
|||
// 验证邮箱
|
|||
RuleFor(x => x.Email) |
|||
.NotEmpty().WithMessage("邮箱不能为空") |
|||
.MaximumLength(256).WithMessage("邮箱长度不能超过256个字符") |
|||
.Must(x => x.IsValidEmail()) |
|||
.WithMessage("邮箱格式不正确"); |
|||
|
|||
// 验证验证码
|
|||
RuleFor(x => x.VerificationCode) |
|||
.NotEmpty().WithMessage("验证码不能为空") |
|||
.Length(6).WithMessage("验证码长度必须为6位") |
|||
.Matches("^[0-9]+$").WithMessage("验证码只能包含数字"); |
|||
} |
|||
} |
@ -1,48 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace CellularManagement.Application.Features.Auth.Common; |
|||
|
|||
/// <summary>
|
|||
/// 用户信息
|
|||
/// </summary>
|
|||
public sealed record UserInfo( |
|||
/// <summary>
|
|||
/// 用户ID
|
|||
/// </summary>
|
|||
string Id, |
|||
|
|||
/// <summary>
|
|||
/// 账号
|
|||
/// </summary>
|
|||
string UserName, |
|||
|
|||
/// <summary>
|
|||
/// 用户名
|
|||
/// </summary>
|
|||
string? RealName, |
|||
|
|||
/// <summary>
|
|||
/// 邮箱
|
|||
/// </summary>
|
|||
string Email, |
|||
|
|||
/// <summary>
|
|||
/// 电话号码
|
|||
/// </summary>
|
|||
string? PhoneNumber, |
|||
|
|||
/// <summary>
|
|||
/// 用户角色列表
|
|||
/// </summary>
|
|||
IList<string> Roles, |
|||
|
|||
/// <summary>
|
|||
/// 用户权限字典
|
|||
/// 键为权限标识符,值为是否拥有该权限
|
|||
/// 例如:
|
|||
/// {
|
|||
/// "dashboard.view": true,
|
|||
/// "users.view": true
|
|||
/// }
|
|||
/// </summary>
|
|||
IDictionary<string, bool> Permissions); |
@ -0,0 +1,21 @@ |
|||
using System; |
|||
|
|||
namespace CellularManagement.Application.Features.Auth.Models; |
|||
|
|||
/// <summary>
|
|||
/// 账号登录响应
|
|||
/// </summary>
|
|||
public class AuthenticateUserResponse : LoginResponse |
|||
{ |
|||
/// <summary>
|
|||
/// 初始化响应
|
|||
/// </summary>
|
|||
public AuthenticateUserResponse( |
|||
string accessToken, |
|||
string refreshToken, |
|||
DateTime expiresAt, |
|||
UserInfo user) |
|||
: base(accessToken, refreshToken, expiresAt, user) |
|||
{ |
|||
} |
|||
} |
@ -0,0 +1,21 @@ |
|||
using System; |
|||
|
|||
namespace CellularManagement.Application.Features.Auth.Models; |
|||
|
|||
/// <summary>
|
|||
/// 邮箱登录响应
|
|||
/// </summary>
|
|||
public class EmailLoginResponse : LoginResponse |
|||
{ |
|||
/// <summary>
|
|||
/// 初始化响应
|
|||
/// </summary>
|
|||
public EmailLoginResponse( |
|||
string accessToken, |
|||
string refreshToken, |
|||
DateTime expiresAt, |
|||
UserInfo user) |
|||
: base(accessToken, refreshToken, expiresAt, user) |
|||
{ |
|||
} |
|||
} |
@ -0,0 +1,44 @@ |
|||
using System; |
|||
|
|||
namespace CellularManagement.Application.Features.Auth.Models; |
|||
|
|||
/// <summary>
|
|||
/// 登录响应基类
|
|||
/// </summary>
|
|||
public abstract class LoginResponse |
|||
{ |
|||
/// <summary>
|
|||
/// 访问令牌
|
|||
/// </summary>
|
|||
public string AccessToken { get; } |
|||
|
|||
/// <summary>
|
|||
/// 刷新令牌
|
|||
/// </summary>
|
|||
public string RefreshToken { get; } |
|||
|
|||
/// <summary>
|
|||
/// 过期时间
|
|||
/// </summary>
|
|||
public DateTime ExpiresAt { get; } |
|||
|
|||
/// <summary>
|
|||
/// 用户信息
|
|||
/// </summary>
|
|||
public UserInfo User { get; } |
|||
|
|||
/// <summary>
|
|||
/// 初始化响应
|
|||
/// </summary>
|
|||
protected LoginResponse( |
|||
string accessToken, |
|||
string refreshToken, |
|||
DateTime expiresAt, |
|||
UserInfo user) |
|||
{ |
|||
AccessToken = accessToken; |
|||
RefreshToken = refreshToken; |
|||
ExpiresAt = expiresAt; |
|||
User = user; |
|||
} |
|||
} |
@ -0,0 +1,65 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace CellularManagement.Application.Features.Auth.Models; |
|||
|
|||
/// <summary>
|
|||
/// 用户信息
|
|||
/// </summary>
|
|||
public class UserInfo |
|||
{ |
|||
/// <summary>
|
|||
/// 用户ID
|
|||
/// </summary>
|
|||
public string Id { get; } |
|||
|
|||
/// <summary>
|
|||
/// 用户名
|
|||
/// </summary>
|
|||
public string UserName { get; } |
|||
|
|||
/// <summary>
|
|||
/// 真实姓名
|
|||
/// </summary>
|
|||
public string? RealName { get; } |
|||
|
|||
/// <summary>
|
|||
/// 邮箱
|
|||
/// </summary>
|
|||
public string Email { get; } |
|||
|
|||
/// <summary>
|
|||
/// 手机号
|
|||
/// </summary>
|
|||
public string? PhoneNumber { get; } |
|||
|
|||
/// <summary>
|
|||
/// 角色列表
|
|||
/// </summary>
|
|||
public IReadOnlyList<string> Roles { get; } |
|||
|
|||
/// <summary>
|
|||
/// 权限字典
|
|||
/// </summary>
|
|||
public IReadOnlyDictionary<string, bool> Permissions { get; } |
|||
|
|||
/// <summary>
|
|||
/// 初始化用户信息
|
|||
/// </summary>
|
|||
public UserInfo( |
|||
string id, |
|||
string userName, |
|||
string? realName, |
|||
string email, |
|||
string? phoneNumber, |
|||
IReadOnlyList<string> roles, |
|||
IReadOnlyDictionary<string, bool> permissions) |
|||
{ |
|||
Id = id; |
|||
UserName = userName; |
|||
RealName = realName; |
|||
Email = email; |
|||
PhoneNumber = phoneNumber; |
|||
Roles = roles; |
|||
Permissions = permissions; |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
using System.Threading.Tasks; |
|||
using CellularManagement.Domain.Common; |
|||
|
|||
namespace CellularManagement.Domain.Services; |
|||
|
|||
/// <summary>
|
|||
/// 验证码验证服务接口
|
|||
/// </summary>
|
|||
public interface ICaptchaVerificationService |
|||
{ |
|||
/// <summary>
|
|||
/// 验证图形验证码
|
|||
/// </summary>
|
|||
/// <param name="captchaId">验证码ID</param>
|
|||
/// <param name="captchaCode">验证码</param>
|
|||
/// <returns>验证结果</returns>
|
|||
Task<(bool success, string? errorMessage)> VerifyCaptchaAsync(string captchaId, string captchaCode); |
|||
} |
@ -0,0 +1,81 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Logging; |
|||
using CellularManagement.Domain.Services; |
|||
using CellularManagement.Domain.Common; |
|||
|
|||
namespace CellularManagement.Infrastructure.Services; |
|||
|
|||
/// <summary>
|
|||
/// 验证码验证服务实现
|
|||
/// </summary>
|
|||
public class CaptchaVerificationService : ICaptchaVerificationService |
|||
{ |
|||
private readonly ICacheService _cacheService; |
|||
private readonly IDistributedLockService _lockService; |
|||
private readonly ILogger<CaptchaVerificationService> _logger; |
|||
|
|||
/// <summary>
|
|||
/// 构造函数
|
|||
/// </summary>
|
|||
/// <param name="cacheService">缓存服务</param>
|
|||
/// <param name="lockService">分布式锁服务</param>
|
|||
/// <param name="logger">日志记录器</param>
|
|||
public CaptchaVerificationService( |
|||
ICacheService cacheService, |
|||
IDistributedLockService lockService, |
|||
ILogger<CaptchaVerificationService> logger) |
|||
{ |
|||
_cacheService = cacheService; |
|||
_lockService = lockService; |
|||
_logger = logger; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 验证图形验证码
|
|||
/// </summary>
|
|||
/// <param name="captchaId">验证码ID</param>
|
|||
/// <param name="captchaCode">验证码</param>
|
|||
/// <returns>验证结果</returns>
|
|||
public async Task<(bool success, string? errorMessage)> VerifyCaptchaAsync(string captchaId, string captchaCode) |
|||
{ |
|||
// 使用分布式锁保护验证码验证过程
|
|||
using var captchaLock = await _lockService.AcquireLockAsync( |
|||
$"captcha_verification_{captchaId}", |
|||
TimeSpan.FromSeconds(5)); |
|||
|
|||
if (!captchaLock.IsAcquired) |
|||
{ |
|||
_logger.LogWarning("验证码验证过程被锁定,请稍后重试"); |
|||
return (false, "系统繁忙,请稍后重试"); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
// 获取缓存的验证码
|
|||
var cachedCaptcha = _cacheService.Get<string>($"captcha:{captchaId}"); |
|||
if (string.IsNullOrEmpty(cachedCaptcha)) |
|||
{ |
|||
_logger.LogWarning("图形验证码已过期或不存在"); |
|||
return (false, "图形验证码已过期或不存在"); |
|||
} |
|||
|
|||
// 验证验证码
|
|||
if (!string.Equals(cachedCaptcha, captchaCode, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
_logger.LogWarning("图形验证码错误"); |
|||
return (false, "图形验证码错误"); |
|||
} |
|||
|
|||
// 验证通过后原子性地删除验证码
|
|||
_cacheService.Remove($"captcha:{captchaId}"); |
|||
|
|||
return (true,string.Empty); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "验证图形验证码时发生异常"); |
|||
return (false, "验证图形验证码时发生错误"); |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue