Browse Source

feat: 同步所有更改,包括认证相关功能和UI更新

norm
hyh 2 months ago
parent
commit
fd1d2b1e70
  1. 3
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommand.cs
  2. 238
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandHandler.cs
  3. 6
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandValidator.cs
  4. 28
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserResponse.cs
  5. 283
      src/CellularManagement.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs
  6. 19
      src/CellularManagement.Application/Features/Auth/Commands/EmailLogin/EmailLoginCommand.cs
  7. 82
      src/CellularManagement.Application/Features/Auth/Commands/EmailLogin/EmailLoginCommandHandler.cs
  8. 29
      src/CellularManagement.Application/Features/Auth/Commands/EmailLogin/EmailLoginCommandValidator.cs
  9. 8
      src/CellularManagement.Application/Features/Auth/Commands/Login/LoginRequestExample.cs
  10. 1
      src/CellularManagement.Application/Features/Auth/Commands/RefreshToken/RefreshTokenCommand.cs
  11. 4
      src/CellularManagement.Application/Features/Auth/Commands/RefreshToken/RefreshTokenCommandHandler.cs
  12. 40
      src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommandHandler.cs
  13. 10
      src/CellularManagement.Application/Features/Auth/Commands/SendVerificationCode/SendVerificationCodeCommand.cs
  14. 12
      src/CellularManagement.Application/Features/Auth/Commands/SendVerificationCode/SendVerificationCodeCommandHandler.cs
  15. 48
      src/CellularManagement.Application/Features/Auth/Common/UserInfo.cs
  16. 21
      src/CellularManagement.Application/Features/Auth/Models/AuthenticateUserResponse.cs
  17. 21
      src/CellularManagement.Application/Features/Auth/Models/EmailLoginResponse.cs
  18. 44
      src/CellularManagement.Application/Features/Auth/Models/LoginResponse.cs
  19. 65
      src/CellularManagement.Application/Features/Auth/Models/UserInfo.cs
  20. 1
      src/CellularManagement.Application/Features/Users/Queries/Dtos/UserDto.cs
  21. 1
      src/CellularManagement.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs
  22. 1
      src/CellularManagement.Application/Features/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs
  23. 18
      src/CellularManagement.Domain/Services/ICaptchaVerificationService.cs
  24. 2
      src/CellularManagement.Infrastructure/DependencyInjection.cs
  25. 81
      src/CellularManagement.Infrastructure/Services/CaptchaVerificationService.cs
  26. 138
      src/CellularManagement.Presentation/Controllers/AuthController.cs
  27. 7
      src/CellularManagement.WebAPI/Program.cs
  28. 96
      src/CellularManagement.WebUI/src/components/auth/LoginForm.tsx
  29. 24
      src/CellularManagement.WebUI/src/components/auth/RegisterForm.tsx
  30. 4
      src/CellularManagement.WebUI/src/constants/auth.ts
  31. 20
      src/CellularManagement.WebUI/src/pages/auth/LoginPage.tsx
  32. 51
      src/CellularManagement.WebUI/src/services/apiService.ts
  33. 12
      src/CellularManagement.WebUI/src/services/authService.ts
  34. 5
      src/CellularManagement.WebUI/src/types/auth.ts

3
src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommand.cs

@ -1,3 +1,4 @@
using CellularManagement.Application.Features.Auth.Models;
using CellularManagement.Domain.Common;
using MediatR;
@ -8,7 +9,7 @@ namespace CellularManagement.Application.Features.Auth.Commands.AuthenticateUser
/// </summary>
public sealed record AuthenticateUserCommand(
/// <summary>
/// 账号或邮箱
/// 账号
/// </summary>
string UserName,

238
src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandHandler.cs

@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging;
using MediatR;
using CellularManagement.Application.Common.Extensions;
using CellularManagement.Domain.Entities;
using CellularManagement.Application.Features.Auth.Common;
using CellularManagement.Domain.Repositories;
using CellularManagement.Domain.Services;
using System.Collections.Generic;
@ -16,24 +15,15 @@ using CellularManagement.Domain.Common;
using Microsoft.AspNetCore.Http;
using UAParser;
using Microsoft.EntityFrameworkCore;
using CellularManagement.Application.Features.Auth.Models;
namespace CellularManagement.Application.Features.Auth.Commands.AuthenticateUser;
/// <summary>
/// 用户认证命令处理器
/// 账号登录命令处理器
/// </summary>
public sealed class AuthenticateUserCommandHandler : IRequestHandler<AuthenticateUserCommand, OperationResult<AuthenticateUserResponse>>
public sealed class AuthenticateUserCommandHandler : BaseLoginCommandHandler<AuthenticateUserCommand, AuthenticateUserResponse>
{
private readonly UserManager<AppUser> _userManager;
private readonly IJwtProvider _jwtProvider;
private readonly ILogger<AuthenticateUserCommandHandler> _logger;
private readonly IUserRoleRepository _userRoleRepository;
private readonly IRolePermissionRepository _rolePermissionRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly ILoginLogRepository _loginLogRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
private const int MaxRetryAttempts = 3;
/// <summary>
/// 初始化处理器
/// </summary>
@ -46,215 +36,41 @@ public sealed class AuthenticateUserCommandHandler : IRequestHandler<Authenticat
IUnitOfWork unitOfWork,
ILoginLogRepository loginLogRepository,
IHttpContextAccessor httpContextAccessor)
: base(userManager, jwtProvider, logger, userRoleRepository, rolePermissionRepository, unitOfWork, loginLogRepository, httpContextAccessor)
{
_userManager = userManager;
_jwtProvider = jwtProvider;
_logger = logger;
_userRoleRepository = userRoleRepository;
_rolePermissionRepository = rolePermissionRepository;
_unitOfWork = unitOfWork;
_loginLogRepository = loginLogRepository;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// 处理认证请求
/// </summary>
public async Task<OperationResult<AuthenticateUserResponse>> Handle(
public override Task<OperationResult<AuthenticateUserResponse>> Handle(
AuthenticateUserCommand 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<AuthenticateUserResponse>.CreateFailure("登录尝试次数过多,请稍后再试");
}
// 解析设备信息
var parser = UAParser.Parser.GetDefault();
var clientInfo = parser.Parse(userAgent);
// 先尝试通过邮箱查找用户
AppUser? user = null;
if (request.UserName.IsValidEmail())
{
user = await _userManager.FindByEmailAsync(request.UserName);
}
// 如果通过邮箱没找到用户,则尝试通过账号查找
if (user == null)
{
user = await _userManager.FindByNameAsync(request.UserName);
}
// 创建登录日志
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("用户 {UserName} 不存在", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("用户名或密码错误");
}
// 检查用户是否已删除
if (user.IsDeleted)
{
loginLog.IsSuccess = false;
loginLog.FailureReason = "用户已被删除";
await _loginLogRepository.AddAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserName} 已被删除", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("用户已被删除");
}
// 检查用户是否已禁用
if (!user.IsActive)
{
loginLog.IsSuccess = false;
loginLog.FailureReason = "用户已被禁用";
await _loginLogRepository.AddAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserName} 已被禁用", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("用户已被禁用");
}
// 验证密码
var isValidPassword = await _userManager.CheckPasswordAsync(user, request.Password);
if (!isValidPassword)
{
loginLog.IsSuccess = false;
loginLog.FailureReason = "密码错误";
await _loginLogRepository.AddAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserName} 密码错误", request.UserName);
return OperationResult<AuthenticateUserResponse>.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("用户 {UserName} 更新时发生并发冲突,重试次数: {RetryCount}",
request.UserName, retryCount);
if (retryCount >= MaxRetryAttempts)
{
_logger.LogError("用户 {UserName} 更新失败,超过最大重试次数", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("系统繁忙,请稍后重试");
}
// 重新获取最新数据
user = await _userManager.FindByIdAsync(user.Id);
if (user == null)
{
return OperationResult<AuthenticateUserResponse>.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);
return HandleLoginAsync(request, cancellationToken);
}
// 创建用户信息
var userInfo = new UserInfo(
user.Id,
user.UserName!,
user.RealName,
user.Email!,
user.PhoneNumber,
roles,
permissions);
/// <summary>
/// 查找用户
/// </summary>
protected override Task<AppUser?> FindUserAsync(AuthenticateUserCommand request)
{
return _userManager.FindByNameAsync(request.UserName);
}
// 记录成功的登录日志
loginLog.IsSuccess = true;
await _loginLogRepository.AddAsync(loginLog, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
_logger.LogInformation("账号 {UserName} 认证成功", request.UserName);
/// <summary>
/// 获取用户标识
/// </summary>
protected override string GetUserIdentifier(AuthenticateUserCommand request)
{
return request.UserName;
}
// 返回认证结果
return OperationResult<AuthenticateUserResponse>.CreateSuccess(
new AuthenticateUserResponse(accessToken, refreshToken, expiresAt, userInfo));
}
catch (Exception ex)
{
_logger.LogError(ex, "用户 {UserName} 认证失败", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("认证失败,请稍后重试");
}
/// <summary>
/// 获取密码
/// </summary>
protected override string GetPassword(AuthenticateUserCommand request)
{
return request.Password;
}
}

6
src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandValidator.cs

@ -14,12 +14,10 @@ public sealed class AuthenticateUserCommandValidator : AbstractValidator<Authent
/// </summary>
public AuthenticateUserCommandValidator()
{
// 验证用户名或邮箱
// 验证账号
RuleFor(x => x.UserName)
.NotEmpty().WithMessage("账号不能为空")
.MaximumLength(256).WithMessage("账号长度不能超过256个字符")
.Must(x => x.Contains('@') ? x.IsValidEmail() : true)
.WithMessage("邮箱格式不正确");
.MaximumLength(256).WithMessage("账号长度不能超过256个字符");
// 验证密码
RuleFor(x => x.Password)

28
src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserResponse.cs

@ -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);

283
src/CellularManagement.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs

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

19
src/CellularManagement.Application/Features/Auth/Commands/EmailLogin/EmailLoginCommand.cs

@ -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>>;

82
src/CellularManagement.Application/Features/Auth/Commands/EmailLogin/EmailLoginCommandHandler.cs

@ -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);
}
}

29
src/CellularManagement.Application/Features/Auth/Commands/EmailLogin/EmailLoginCommandValidator.cs

@ -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("验证码只能包含数字");
}
}

8
src/CellularManagement.Application/Features/Auth/Commands/Login/LoginRequestExample.cs

@ -15,8 +15,8 @@ public class LoginRequestExample : IExamplesProvider<LoginRequest>
{
return new LoginRequest
{
UserName = "zhangsan",
Password = "Zhangsan0001@qq.com"
UserName = "hyh",
Password = "Hyh@123456"
};
}
}
@ -37,8 +37,8 @@ public class LoginRequestExamples : IMultipleExamplesProvider<LoginRequest>
"使用默认测试账号",
new LoginRequest
{
UserName = "zhangsan",
Password = "Zhangsan0001@qq.com"
UserName = "hyh",
Password = "Hyh@123456"
}
);
}

1
src/CellularManagement.Application/Features/Auth/Commands/RefreshToken/RefreshTokenCommand.cs

@ -2,6 +2,7 @@ using MediatR;
using System.ComponentModel.DataAnnotations;
using CellularManagement.Application.Features.Auth.Commands.AuthenticateUser;
using CellularManagement.Domain.Common;
using CellularManagement.Application.Features.Auth.Models;
namespace CellularManagement.Application.Features.Auth.Commands.RefreshToken;

4
src/CellularManagement.Application/Features/Auth/Commands/RefreshToken/RefreshTokenCommandHandler.cs

@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging;
using CellularManagement.Application.Common;
using System.Security.Claims;
using CellularManagement.Application.Features.Auth.Commands.AuthenticateUser;
using CellularManagement.Application.Features.Auth.Common;
using CellularManagement.Domain.Repositories;
using CellularManagement.Domain.Services;
using System.Threading.Tasks;
@ -12,6 +11,7 @@ using System.Linq;
using System.Collections.Generic;
using System;
using CellularManagement.Domain.Common;
using CellularManagement.Application.Features.Auth.Models;
namespace CellularManagement.Application.Features.Auth.Commands.RefreshToken;
@ -132,7 +132,7 @@ public sealed class RefreshTokenCommandHandler : IRequestHandler<RefreshTokenCom
claims.FirstOrDefault(c => c.Type == "RealName")?.Value,
claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value ?? string.Empty,
claims.FirstOrDefault(c => c.Type == ClaimTypes.MobilePhone)?.Value,
roles,
roles.ToList().AsReadOnly(),
permissions);
_logger.LogInformation("刷新令牌成功");

40
src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommandHandler.cs

@ -18,59 +18,35 @@ namespace CellularManagement.Application.Features.Auth.Commands.RegisterUser;
public sealed class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, OperationResult<RegisterUserResponse>>
{
private readonly IUserRegistrationService _userRegistrationService;
private readonly ICaptchaVerificationService _captchaVerificationService;
private readonly ILogger<RegisterUserCommandHandler> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly ICacheService _cacheService;
private readonly IDistributedLockService _lockService;
public RegisterUserCommandHandler(
IUserRegistrationService userRegistrationService,
ICaptchaVerificationService captchaVerificationService,
ILogger<RegisterUserCommandHandler> logger,
IUnitOfWork unitOfWork,
ICacheService cacheService,
IDistributedLockService lockService)
IUnitOfWork unitOfWork)
{
_userRegistrationService = userRegistrationService;
_captchaVerificationService = captchaVerificationService;
_logger = logger;
_unitOfWork = unitOfWork;
_cacheService = cacheService;
_lockService = lockService;
}
public async Task<OperationResult<RegisterUserResponse>> Handle(
RegisterUserCommand request,
CancellationToken cancellationToken)
{
// 使用分布式锁保护验证码验证过程
using var captchaLock = await _lockService.AcquireLockAsync(
$"captcha_verification_{request.CaptchaId}",
TimeSpan.FromSeconds(5));
if (!captchaLock.IsAcquired)
{
_logger.LogWarning("验证码验证过程被锁定,请稍后重试");
return OperationResult<RegisterUserResponse>.CreateFailure("系统繁忙,请稍后重试");
}
try
{
// 验证验证码
var cachedCaptcha = _cacheService.Get<string>($"captcha:{request.CaptchaId}");
if (string.IsNullOrEmpty(cachedCaptcha))
// 验证图形验证码
var(IsSuccess, errorMessage) = await _captchaVerificationService.VerifyCaptchaAsync(request.CaptchaId, request.CaptchaCode);
if (!IsSuccess)
{
_logger.LogWarning("验证码已过期或不存在");
return OperationResult<RegisterUserResponse>.CreateFailure("验证码已过期或不存在");
return OperationResult<RegisterUserResponse>.CreateFailure(errorMessage?? "验证码验证失败");
}
if (!string.Equals(cachedCaptcha, request.CaptchaCode, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("验证码错误");
return OperationResult<RegisterUserResponse>.CreateFailure("验证码错误");
}
// 验证通过后原子性地删除验证码
_cacheService.Remove($"captcha:{request.CaptchaId}");
// 创建用户实体
var user = new AppUser
{

10
src/CellularManagement.Application/Features/Auth/Commands/SendVerificationCode/SendVerificationCodeCommand.cs

@ -13,6 +13,16 @@ public sealed record SendVerificationCodeCommand : IRequest<OperationResult<Send
/// 目标邮箱地址
/// </summary>
public string Email { get; init; } = string.Empty;
/// <summary>
/// 图形验证码ID
/// </summary>
public string CaptchaId { get; init; } = string.Empty;
/// <summary>
/// 图形验证码
/// </summary>
public string CaptchaCode { get; init; } = string.Empty;
}
/// <summary>

12
src/CellularManagement.Application/Features/Auth/Commands/SendVerificationCode/SendVerificationCodeCommandHandler.cs

@ -6,6 +6,7 @@ using CellularManagement.Domain.Common;
using CellularManagement.Domain.Options;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
namespace CellularManagement.Application.Features.Auth.Commands.SendVerificationCode;
@ -16,6 +17,7 @@ namespace CellularManagement.Application.Features.Auth.Commands.SendVerification
public sealed class SendVerificationCodeCommandHandler : IRequestHandler<SendVerificationCodeCommand, OperationResult<SendVerificationCodeResponse>>
{
private readonly IEmailVerificationService _emailVerificationService;
private readonly ICaptchaVerificationService _captchaVerificationService;
private readonly ILogger<SendVerificationCodeCommandHandler> _logger;
private readonly EmailVerificationOptions _options;
@ -23,14 +25,17 @@ public sealed class SendVerificationCodeCommandHandler : IRequestHandler<SendVer
/// 构造函数
/// </summary>
/// <param name="emailVerificationService">邮箱验证服务</param>
/// <param name="captchaVerificationService">验证码验证服务</param>
/// <param name="logger">日志记录器</param>
/// <param name="options">邮箱验证码配置选项</param>
public SendVerificationCodeCommandHandler(
IEmailVerificationService emailVerificationService,
ICaptchaVerificationService captchaVerificationService,
ILogger<SendVerificationCodeCommandHandler> logger,
IOptions<EmailVerificationOptions> options)
{
_emailVerificationService = emailVerificationService;
_captchaVerificationService = captchaVerificationService;
_logger = logger;
_options = options.Value;
}
@ -47,6 +52,13 @@ public sealed class SendVerificationCodeCommandHandler : IRequestHandler<SendVer
{
try
{
// 验证图形验证码
var(IsSuccess, errorMessage) = await _captchaVerificationService.VerifyCaptchaAsync(request.CaptchaId, request.CaptchaCode);
if (!IsSuccess)
{
return OperationResult<SendVerificationCodeResponse>.CreateFailure(errorMessage ?? "验证码验证失败");
}
// 记录开始发送验证码
_logger.LogInformation("开始向邮箱 {Email} 发送验证码", request.Email);

48
src/CellularManagement.Application/Features/Auth/Common/UserInfo.cs

@ -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);

21
src/CellularManagement.Application/Features/Auth/Models/AuthenticateUserResponse.cs

@ -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)
{
}
}

21
src/CellularManagement.Application/Features/Auth/Models/EmailLoginResponse.cs

@ -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)
{
}
}

44
src/CellularManagement.Application/Features/Auth/Models/LoginResponse.cs

@ -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;
}
}

65
src/CellularManagement.Application/Features/Auth/Models/UserInfo.cs

@ -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;
}
}

1
src/CellularManagement.Application/Features/Users/Queries/Dtos/UserDto.cs

@ -11,5 +11,6 @@ namespace CellularManagement.Application.Features.Users.Queries.Dtos;
public sealed record UserDto(
string UserId,
string UserName,
string RealName,
string Email,
string PhoneNumber, DateTime CreatedAt, bool IsActive);

1
src/CellularManagement.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs

@ -78,6 +78,7 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler<GetAllUsersQuery,
var userDtos = users.Select(user => new UserDto(
user.Id,
user.UserName,
user.RealName,
user.Email,
user.PhoneNumber,user.CreatedTime,user.IsActive)).ToList();

1
src/CellularManagement.Application/Features/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs

@ -53,6 +53,7 @@ public sealed class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery,
}
var dto = new UserDto(user.Id,
user.UserName,
user.RealName,
user.Email,
user.PhoneNumber,user.CreatedTime, user.IsActive);

18
src/CellularManagement.Domain/Services/ICaptchaVerificationService.cs

@ -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);
}

2
src/CellularManagement.Infrastructure/DependencyInjection.cs

@ -122,7 +122,7 @@ public static class DependencyInjection
// 注册缓存服务
services.AddScoped<ICacheService, CacheService>();
services.AddScoped<ICaptchaService, CaptchaService>();
services.AddScoped<ICaptchaVerificationService, CaptchaVerificationService>();
// 配置认证设置
services.Configure<AuthConfiguration>(configuration.GetSection("Auth"));

81
src/CellularManagement.Infrastructure/Services/CaptchaVerificationService.cs

@ -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, "验证图形验证码时发生错误");
}
}
}

138
src/CellularManagement.Presentation/Controllers/AuthController.cs

@ -17,6 +17,9 @@ using CellularManagement.Domain.Common;
using CellularManagement.Application.Features.Auth.Commands.SendVerificationCode;
using CellularManagement.Application.Features.Auth.Commands.VerifyCode;
using CellularManagement.Application.Features.Auth.Commands.GenerateCaptcha;
using CellularManagement.Application.Features.Auth.Models;
using CellularManagement.Application.Features.Auth.Commands.EmailLogin;
using CellularManagement.Application.Features.Auth.Commands.Logout;
namespace CellularManagement.Presentation.Controllers;
@ -52,19 +55,19 @@ public class AuthController : ApiController
}
/// <summary>
/// 用户登录
/// 账号登录
/// </summary>
/// <remarks>
/// 示例请求:
///
/// POST /api/auth/login
/// {
/// "userNameOrEmail": "用户名或邮箱",
/// "userName": "账号",
/// "password": "密码"
/// }
///
/// </remarks>
/// <param name="command">登录命令,包含用户名/邮箱和密码</param>
/// <param name="command">登录命令,包含账号和密码</param>
/// <returns>
/// 登录结果,包含:
/// - 成功:返回用户信息和访问令牌
@ -88,7 +91,7 @@ public class AuthController : ApiController
if (attempts >= _authConfig.MaxLoginAttempts)
{
_logger.LogWarning("用户 {UserName} 登录尝试次数过多", command.UserName);
_logger.LogWarning("账号 {UserName} 登录尝试次数过多", command.UserName);
return StatusCode(StatusCodes.Status429TooManyRequests,
OperationResult<AuthenticateUserResponse>.CreateFailure("登录尝试次数过多,请稍后再试"));
}
@ -103,7 +106,7 @@ public class AuthController : ApiController
.SetAbsoluteExpiration(TimeSpan.FromMinutes(_authConfig.LoginAttemptsWindowMinutes));
_cache.Set(cacheKey, attempts + 1, options);
_logger.LogWarning("用户 {UserName} 登录失败: {Error}",
_logger.LogWarning("账号 {UserName} 登录失败: {Error}",
command.UserName,
result.ErrorMessages?.FirstOrDefault());
}
@ -111,14 +114,86 @@ public class AuthController : ApiController
{
// 清除登录尝试次数
_cache.Remove(cacheKey);
_logger.LogInformation("用户 {UserName} 登录成功", command.UserName);
_logger.LogInformation("账号 {UserName} 登录成功", command.UserName);
}
_logger.LogWarning($"Bearer {result.Data.AccessToken}");
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "用户 {UserName} 登录时发生异常", command.UserName);
_logger.LogError(ex, "账号 {UserName} 登录时发生异常", command.UserName);
return StatusCode(StatusCodes.Status500InternalServerError,
OperationResult<AuthenticateUserResponse>.CreateFailure("系统错误,请稍后重试"));
}
}
/// <summary>
/// 邮箱登录
/// </summary>
/// <remarks>
/// 示例请求:
///
/// POST /api/auth/login/email
/// {
/// "email": "邮箱",
/// "password": "密码"
/// }
///
/// </remarks>
/// <param name="command">邮箱登录命令,包含邮箱和密码</param>
/// <returns>
/// 登录结果,包含:
/// - 成功:返回用户信息和访问令牌
/// - 失败:返回错误信息
/// </returns>
/// <response code="200">登录成功,返回用户信息和令牌</response>
/// <response code="400">登录失败,返回错误信息</response>
/// <response code="429">登录尝试次数过多</response>
[HttpPost("email")]
[ProducesResponseType(typeof(OperationResult<AuthenticateUserResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<AuthenticateUserResponse>), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(OperationResult<AuthenticateUserResponse>), StatusCodes.Status429TooManyRequests)]
public async Task<ActionResult<OperationResult<AuthenticateUserResponse>>> LoginWithEmail([FromBody] EmailLoginCommand command)
{
try
{
// 检查登录尝试次数
var cacheKey = string.Format(_authConfig.LoginAttemptsCacheKeyFormat, command.Email);
var attempts = _cache.Get<int>(cacheKey);
if (attempts >= _authConfig.MaxLoginAttempts)
{
_logger.LogWarning("邮箱 {Email} 登录尝试次数过多", command.Email);
return StatusCode(StatusCodes.Status429TooManyRequests,
OperationResult<AuthenticateUserResponse>.CreateFailure("登录尝试次数过多,请稍后再试"));
}
// 执行登录
var result = await mediator.Send(command);
if (!result.IsSuccess)
{
// 增加登录尝试次数
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(_authConfig.LoginAttemptsWindowMinutes));
_cache.Set(cacheKey, attempts + 1, options);
_logger.LogWarning("邮箱 {Email} 登录失败: {Error}",
command.Email,
result.ErrorMessages?.FirstOrDefault());
}
else
{
// 清除登录尝试次数
_cache.Remove(cacheKey);
_logger.LogInformation("邮箱 {Email} 登录成功", command.Email);
}
_logger.LogWarning($"Bearer {result.Data.AccessToken}");
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "邮箱 {Email} 登录时发生异常", command.Email);
return StatusCode(StatusCodes.Status500InternalServerError,
OperationResult<AuthenticateUserResponse>.CreateFailure("系统错误,请稍后重试"));
}
@ -377,4 +452,53 @@ public class AuthController : ApiController
OperationResult<GenerateCaptchaResponse>.CreateFailure("系统错误,请稍后重试"));
}
}
/// <summary>
/// 登出
/// </summary>
/// <remarks>
/// 示例请求:
///
/// POST /api/auth/logout
/// {
/// "refreshToken": "刷新令牌"
/// }
///
/// </remarks>
/// <param name="command">登出命令,包含刷新令牌</param>
/// <returns>
/// 登出结果,包含:
/// - 成功:返回成功信息
/// - 失败:返回错误信息
/// </returns>
/// <response code="200">登出成功</response>
/// <response code="400">登出失败,返回错误信息</response>
[HttpPost("logout")]
[ProducesResponseType(typeof(OperationResult<bool>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<bool>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OperationResult<bool>>> Logout([FromBody] LogoutCommand command)
{
try
{
var result = await mediator.Send(command);
if (result.IsSuccess)
{
_logger.LogInformation("用户登出成功");
}
else
{
_logger.LogWarning("用户登出失败: {Error}",
result.ErrorMessages?.FirstOrDefault());
}
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "用户登出时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError,
OperationResult<bool>.CreateFailure("系统错误,请稍后重试"));
}
}
}

7
src/CellularManagement.WebAPI/Program.cs

@ -110,13 +110,6 @@ builder.Services.ConfigureSwaggerGen(options =>
// 启用 Swagger 注释
builder.Services.AddSwaggerGenNewtonsoftSupport();
// 注册 MediatR 服务
// 从 WebAPI 程序集加载处理器,用于处理 Web 层特定的命令和查询
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});
// 添加 CORS 服务
// 配置跨域资源共享支持
builder.Services.Configure<CorsOptions>(builder.Configuration.GetSection(CorsOptions.SectionName));

96
src/CellularManagement.WebUI/src/components/auth/LoginForm.tsx

@ -22,6 +22,22 @@ export function LoginForm({ onSubmit }: LoginFormProps) {
const [captchaId, setCaptchaId] = useState<string>('');
const [captchaAvailable, setCaptchaAvailable] = useState(false);
const [captchaCode, setCaptchaCode] = useState('');
const [countdown, setCountdown] = useState(0);
// 倒计时效果
useEffect(() => {
let timer: NodeJS.Timeout;
if (countdown > 0) {
timer = setInterval(() => {
setCountdown((prev) => prev - 1);
}, 1000);
}
return () => {
if (timer) {
clearInterval(timer);
}
};
}, [countdown]);
// 获取验证码
const fetchCaptcha = async () => {
@ -58,23 +74,44 @@ export function LoginForm({ onSubmit }: LoginFormProps) {
// 组件加载时获取验证码
useEffect(() => {
fetchCaptcha();
}, []);
if (loginType === 'email') {
fetchCaptcha();
}
}, [loginType]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await onSubmit(
loginType === 'account' ? account : email,
password,
rememberMe,
loginType,
loginType === 'email' ? verificationCode : undefined,
captchaId,
captchaCode
);
if (loginType === 'email') {
await onSubmit(
email,
'', // 邮箱登录不需要密码
rememberMe,
loginType,
verificationCode,
captchaId,
captchaCode
);
} else {
await onSubmit(
account,
password,
rememberMe,
loginType
);
}
};
const handleSendVerificationCode = async () => {
if (!email) {
toast({
title: '请输入邮箱',
description: '请输入QQ邮箱地址',
variant: 'destructive',
duration: 3000,
});
return;
}
if (!captchaAvailable || !captchaCode) {
toast({
title: '请先完成验证码验证',
@ -84,8 +121,37 @@ export function LoginForm({ onSubmit }: LoginFormProps) {
});
return;
}
// TODO: 实现发送验证码的逻辑
console.log('发送验证码到邮箱:', email);
try {
const response = await apiService.sendVerificationCode({
email: `${email}@qq.com`,
captchaId,
captchaCode
});
if (response.success) {
setCountdown(120); // 设置120秒倒计时
toast({
title: '验证码已发送',
description: '请查收您的QQ邮箱',
duration: 3000,
});
} else {
toast({
title: '发送失败',
description: response.message || '请稍后重试',
variant: 'destructive',
duration: 3000,
});
}
} catch (error) {
toast({
title: '发送失败',
description: '请稍后重试',
variant: 'destructive',
duration: 3000,
});
}
};
return (
@ -304,9 +370,9 @@ export function LoginForm({ onSubmit }: LoginFormProps) {
className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center
whitespace-nowrap px-2 text-sm font-medium text-primary hover:text-primary/80
transition-all duration-200 focus:outline-none disabled:opacity-50"
disabled={isLoading || !captchaAvailable || !captchaCode}
disabled={isLoading || !captchaAvailable || !captchaCode || countdown > 0}
>
{countdown > 0 ? `${countdown}秒后重试` : '获取验证码'}
</button>
</div>
</div>

24
src/CellularManagement.WebUI/src/components/auth/RegisterForm.tsx

@ -9,7 +9,7 @@ const registerSchema = z.object({
.min(3, '账号长度不能少于3个字符')
.max(50, '账号长度不能超过50个字符')
.regex(/^[a-zA-Z0-9_]+$/, '账号只能包含字母、数字和下划线'),
displayName: z.string()
realName: z.string()
.min(2, '用户名长度不能少于2个字符')
.max(20, '用户名长度不能超过20个字符'),
email: z.string()
@ -38,7 +38,7 @@ const registerSchema = z.object({
export interface RegisterRequest {
userName: string;
displayName: string;
realName: string;
email: string;
password: string;
confirmPassword: string;
@ -54,7 +54,7 @@ interface RegisterFormProps {
export function RegisterForm({ onSubmit }: RegisterFormProps) {
const [formData, setFormData] = useState({
username: '',
displayName: '',
realName: '',
email: '',
password: '',
confirmPassword: '',
@ -192,7 +192,7 @@ export function RegisterForm({ onSubmit }: RegisterFormProps) {
try {
const req: RegisterRequest = {
userName: formData.username,
displayName: formData.displayName,
realName: formData.realName,
email: formData.email,
password: formData.password,
confirmPassword: formData.confirmPassword,
@ -243,29 +243,29 @@ export function RegisterForm({ onSubmit }: RegisterFormProps) {
</div>
<div className="flex items-center group">
<label htmlFor="displayName" className="w-12 text-sm font-medium text-gray-700 group-focus-within:text-primary transition-colors duration-200 text-right pr-4 flex items-center justify-end">
<label htmlFor="realName" className="w-12 text-sm font-medium text-gray-700 group-focus-within:text-primary transition-colors duration-200 text-right pr-4 flex items-center justify-end">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</label>
<div className="flex-1">
<input
id="displayName"
name="displayName"
id="realName"
name="realName"
type="text"
required
value={formData.displayName}
value={formData.realName}
onChange={handleChange}
className={`block w-full rounded-lg border ${
errors.displayName ? 'border-red-500' : 'border-gray-300'
errors.realName ? 'border-red-500' : 'border-gray-300'
} bg-white px-4 py-2.5 text-sm shadow-sm transition-all duration-200
focus:border-primary focus:ring-2 focus:ring-primary/10
placeholder:text-gray-400 disabled:bg-gray-50 disabled:text-gray-500`}
placeholder="请输入用户名"
placeholder="请输入真实姓名"
disabled={isLoading}
/>
{errors.displayName && (
<p className="mt-1.5 text-sm text-red-500">{errors.displayName}</p>
{errors.realName && (
<p className="mt-1.5 text-sm text-red-500">{errors.realName}</p>
)}
</div>
</div>

4
src/CellularManagement.WebUI/src/constants/auth.ts

@ -46,6 +46,6 @@ export const AUTH_CONSTANTS = {
} as const;
export const DEFAULT_CREDENTIALS = {
username: 'zhangsan',
password: 'Zhangsan0001@qq.com'
username: 'hyh',
password: 'Hyh@123456'
};

20
src/CellularManagement.WebUI/src/pages/auth/LoginPage.tsx

@ -12,10 +12,26 @@ export function LoginPage() {
password: string,
rememberMe: boolean,
loginType: 'account' | 'email',
verificationCode?: string
verificationCode?: string,
captchaId?: string,
captchaCode?: string
) => {
try {
await login({ username, password, rememberMe, loginType, verificationCode });
if (loginType === 'email') {
await login({
email: `${username}@qq.com`,
verificationCode: verificationCode || '',
rememberMe,
loginType
});
} else {
await login({
username,
password,
rememberMe,
loginType
});
}
const from = (location.state as any)?.from?.pathname || '/dashboard';
navigate(from, { replace: true });
} catch (error) {

51
src/CellularManagement.WebUI/src/services/apiService.ts

@ -16,6 +16,7 @@ interface ApiResponse<T> {
export interface ApiService {
login: (request: LoginRequest) => Promise<OperationResult<LoginResponse>>;
emailLogin: (request: { email: string; verificationCode: string }) => Promise<OperationResult<LoginResponse>>;
register: (request: RegisterRequest) => Promise<OperationResult<void>>;
refreshToken: (refreshToken: string) => Promise<OperationResult<LoginResponse>>;
logout: () => Promise<OperationResult<void>>;
@ -23,6 +24,7 @@ export interface ApiService {
getCaptcha: () => Promise<OperationResult<CaptchaResponse>>;
sendResetPasswordEmail: (email: string, captchaId: string, captchaCode: string) => Promise<OperationResult<void>>;
resetPassword: (email: string, verificationCode: string, newPassword: string) => Promise<OperationResult<void>>;
sendVerificationCode: (request: { email: string; captchaId: string; captchaCode: string }) => Promise<OperationResult<void>>;
}
class ApiError extends Error {
@ -56,6 +58,29 @@ export const apiService: ApiService = {
}
},
emailLogin: async (request: { email: string; verificationCode: string }): Promise<OperationResult<LoginResponse>> => {
try {
const response = await httpClient.post<ApiResponse<LoginResponse>>('/auth/email', request);
if (response.isSuccess) {
return {
success: true,
data: response.data,
message: response.successMessage || AUTH_CONSTANTS.MESSAGES.LOGIN_SUCCESS
};
} else {
return {
success: false,
message: response?.errorMessages?.join(', ') || AUTH_CONSTANTS.MESSAGES.LOGIN_FAILED
};
}
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || AUTH_CONSTANTS.MESSAGES.LOGIN_FAILED
};
}
},
register: async (request: RegisterRequest): Promise<OperationResult<void>> => {
try {
const response = await httpClient.post<ApiResponse<void>>('/auth/register', request);
@ -171,7 +196,7 @@ export const apiService: ApiService = {
sendResetPasswordEmail: async (email: string, captchaId: string, captchaCode: string): Promise<OperationResult<void>> => {
try {
const response = await httpClient.post<ApiResponse<void>>('/api/auth/reset-password/email', {
const response = await httpClient.post<ApiResponse<void>>('/auth/reset-password/email', {
email: `${email}@qq.com`,
captchaId,
captchaCode
@ -197,7 +222,7 @@ export const apiService: ApiService = {
resetPassword: async (email: string, verificationCode: string, newPassword: string): Promise<OperationResult<void>> => {
try {
const response = await httpClient.post<ApiResponse<void>>('/api/auth/reset-password', {
const response = await httpClient.post<ApiResponse<void>>('/auth/reset-password', {
email: `${email}@qq.com`,
verificationCode,
newPassword
@ -219,5 +244,27 @@ export const apiService: ApiService = {
message: error.response?.data?.message || AUTH_CONSTANTS.MESSAGES.PASSWORD_RESET_FAILED
};
}
},
sendVerificationCode: async (request: { email: string; captchaId: string; captchaCode: string }): Promise<OperationResult<void>> => {
try {
const response = await httpClient.post<ApiResponse<void>>('/auth/verification-codes', request);
if (response.isSuccess) {
return {
success: true,
message: response.successMessage || '验证码发送成功'
};
} else {
return {
success: false,
message: response?.errorMessages?.join(', ') || '验证码发送失败'
};
}
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || '验证码发送失败'
};
}
}
};

12
src/CellularManagement.WebUI/src/services/authService.ts

@ -58,7 +58,17 @@ export const authService: AuthService = {
try {
console.log('[authService] handleLogin start', request);
dispatch({ type: 'LOGIN_START' });
const result = await apiService.login(request);
let result;
if (request.loginType === 'email') {
result = await apiService.emailLogin({
email: request.email!,
verificationCode: request.verificationCode!
});
} else {
result = await apiService.login(request);
}
console.log('[authService] login result', result);
if (!result.success) {
throw new AuthError(result.message || AUTH_CONSTANTS.MESSAGES.LOGIN_FAILED);

5
src/CellularManagement.WebUI/src/types/auth.ts

@ -8,8 +8,9 @@ export interface User {
}
export interface LoginRequest {
username: string;
password: string;
username?: string;
email?: string;
password?: string;
rememberMe: boolean;
loginType: 'account' | 'email';
verificationCode?: string;

Loading…
Cancel
Save