Browse Source

refactor: 规范化代码结构和实现

norm
root 3 months ago
parent
commit
85930db010
  1. 57
      src/CellularManagement.Application/Common/Extensions/EmailValidationExtensions.cs
  2. 28
      src/CellularManagement.Application/Common/StringExtensions.cs
  3. 11
      src/CellularManagement.Application/DependencyInjection.cs
  4. 2
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandHandler.cs
  5. 1
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandValidator.cs
  6. 2
      src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommandValidator.cs
  7. 33
      src/CellularManagement.Application/Features/Auth/Commands/SendVerificationCode/SendVerificationCodeCommand.cs
  8. 80
      src/CellularManagement.Application/Features/Auth/Commands/SendVerificationCode/SendVerificationCodeCommandHandler.cs
  9. 38
      src/CellularManagement.Application/Features/Auth/Commands/VerifyCode/VerifyCodeCommand.cs
  10. 78
      src/CellularManagement.Application/Features/Auth/Commands/VerifyCode/VerifyCodeCommandHandler.cs
  11. 2
      src/CellularManagement.Application/Features/Users/Commands/CreateUser/CreateUserCommandValidator.cs
  12. 2
      src/CellularManagement.Domain/Options/DatabaseOptions.cs
  13. 2
      src/CellularManagement.Domain/Options/EmailOptions.cs
  14. 27
      src/CellularManagement.Domain/Options/EmailVerificationOptions.cs
  15. 134
      src/CellularManagement.Domain/Options/JwtOptions.cs
  16. 22
      src/CellularManagement.Domain/Services/JwtValidationService.cs
  17. 10
      src/CellularManagement.Infrastructure/DependencyInjection.cs
  18. 17
      src/CellularManagement.Infrastructure/Options/JwtBearerOptionsSetup.cs
  19. 1
      src/CellularManagement.Infrastructure/Services/EmailService.cs
  20. 50
      src/CellularManagement.Infrastructure/Services/EmailVerificationService.cs
  21. 14
      src/CellularManagement.Infrastructure/Services/JwtProvider.cs
  22. 135
      src/CellularManagement.Infrastructure/Services/JwtValidationService.cs
  23. 9
      src/CellularManagement.Infrastructure/Services/KeyRotationService.cs
  24. 115
      src/CellularManagement.Presentation/Controllers/AuthController.cs
  25. 2
      src/CellularManagement.Presentation/Controllers/PermissionsController.cs
  26. 2
      src/CellularManagement.Presentation/Controllers/RolePermissionController.cs
  27. 2
      src/CellularManagement.Presentation/Controllers/RolesController.cs
  28. 4
      src/CellularManagement.Presentation/Controllers/UsersController.cs
  29. 3
      src/CellularManagement.WebAPI/Program.cs
  30. 13
      src/CellularManagement.WebAPI/appsettings.json
  31. 10
      src/CellularManagement.WebUI/src/services/apiService.ts
  32. 8
      src/CellularManagement.WebUI/src/services/roleService.ts

57
src/CellularManagement.Application/Common/Extensions/EmailValidationExtensions.cs

@ -0,0 +1,57 @@
using System.Text.RegularExpressions;
namespace CellularManagement.Application.Common.Extensions;
/// <summary>
/// 邮箱验证扩展方法
/// 提供邮箱地址格式验证的功能
/// </summary>
public static class EmailValidationExtensions
{
/// <summary>
/// RFC 5322 标准的邮箱验证正则表达式
/// </summary>
private static readonly Regex EmailRegex = new(
@"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// 验证邮箱地址格式是否有效
/// </summary>
/// <remarks>
/// 验证规则:
/// 1. 不能为空或空白字符
/// 2. 必须符合 RFC 5322 标准格式
/// 3. 本地部分(@前)长度不超过64个字符
/// 4. 域名部分(@后)长度不超过255个字符
/// </remarks>
/// <param name="email">要验证的邮箱地址</param>
/// <returns>如果邮箱地址格式有效返回 true,否则返回 false</returns>
public static bool IsValidEmail(this string email)
{
if (string.IsNullOrWhiteSpace(email))
{
return false;
}
// 检查邮箱长度
if (email.Length > 254) // RFC 5321 规定的最大长度
{
return false;
}
// 检查本地部分和域名部分的长度
var parts = email.Split('@');
if (parts.Length != 2)
{
return false;
}
if (parts[0].Length > 64 || parts[1].Length > 255)
{
return false;
}
return EmailRegex.IsMatch(email);
}
}

28
src/CellularManagement.Application/Common/StringExtensions.cs

@ -1,28 +0,0 @@
using System.Text.RegularExpressions;
namespace CellularManagement.Application.Common;
/// <summary>
/// 字符串扩展方法
/// </summary>
public static class StringExtensions
{
private static readonly Regex EmailRegex = new(
@"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$",
RegexOptions.Compiled);
/// <summary>
/// 验证邮箱格式
/// </summary>
/// <param name="email">邮箱地址</param>
/// <returns>是否有效</returns>
public static bool IsValidEmail(this string email)
{
if (string.IsNullOrWhiteSpace(email))
{
return false;
}
return EmailRegex.IsMatch(email);
}
}

11
src/CellularManagement.Application/DependencyInjection.cs

@ -3,6 +3,8 @@ using MediatR;
using FluentValidation;
using CellularManagement.Application.Behaviours;
using CellularManagement.Domain.Services;
using Microsoft.Extensions.Configuration;
using CellularManagement.Domain.Options;
namespace CellularManagement.Application;
@ -17,9 +19,15 @@ public static class DependencyInjection
/// 包括命令/查询处理、验证等服务
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configuration">配置</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddApplication(this IServiceCollection services)
public static IServiceCollection AddApplication(
this IServiceCollection services,
IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
// 获取当前程序集
var assembly = typeof(DependencyInjection).Assembly;
@ -34,7 +42,6 @@ public static class DependencyInjection
// 注册验证器
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
return services;
}
}

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

@ -2,7 +2,7 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using MediatR;
using CellularManagement.Application.Common;
using CellularManagement.Application.Common.Extensions;
using CellularManagement.Domain.Entities;
using CellularManagement.Application.Features.Auth.Common;
using CellularManagement.Domain.Repositories;

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

@ -1,5 +1,6 @@
using FluentValidation;
using CellularManagement.Application.Common;
using CellularManagement.Application.Common.Extensions;
namespace CellularManagement.Application.Features.Auth.Commands.AuthenticateUser;

2
src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommandValidator.cs

@ -1,5 +1,5 @@
using FluentValidation;
using CellularManagement.Application.Common;
using CellularManagement.Application.Common.Extensions;
namespace CellularManagement.Application.Features.Auth.Commands.RegisterUser;

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

@ -0,0 +1,33 @@
using MediatR;
using CellularManagement.Domain.Common;
namespace CellularManagement.Application.Features.Auth.Commands.SendVerificationCode;
/// <summary>
/// 发送验证码命令
/// 用于请求向指定邮箱发送验证码
/// </summary>
public sealed record SendVerificationCodeCommand : IRequest<OperationResult<SendVerificationCodeResponse>>
{
/// <summary>
/// 目标邮箱地址
/// </summary>
public string Email { get; init; } = string.Empty;
}
/// <summary>
/// 发送验证码响应
/// 包含验证码发送的结果信息
/// </summary>
public sealed record SendVerificationCodeResponse
{
/// <summary>
/// 是否发送成功
/// </summary>
public bool Success { get; init; }
/// <summary>
/// 验证码有效期(分钟)
/// </summary>
public int ExpirationMinutes { get; init; }
}

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

@ -0,0 +1,80 @@
using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using CellularManagement.Domain.Services;
using CellularManagement.Domain.Common;
using CellularManagement.Domain.Options;
using System.Threading;
using System.Threading.Tasks;
namespace CellularManagement.Application.Features.Auth.Commands.SendVerificationCode;
/// <summary>
/// 发送验证码命令处理器
/// 处理发送验证码的业务逻辑
/// </summary>
public sealed class SendVerificationCodeCommandHandler : IRequestHandler<SendVerificationCodeCommand, OperationResult<SendVerificationCodeResponse>>
{
private readonly IEmailVerificationService _emailVerificationService;
private readonly ILogger<SendVerificationCodeCommandHandler> _logger;
private readonly EmailVerificationOptions _options;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="emailVerificationService">邮箱验证服务</param>
/// <param name="logger">日志记录器</param>
/// <param name="options">邮箱验证码配置选项</param>
public SendVerificationCodeCommandHandler(
IEmailVerificationService emailVerificationService,
ILogger<SendVerificationCodeCommandHandler> logger,
IOptions<EmailVerificationOptions> options)
{
_emailVerificationService = emailVerificationService;
_logger = logger;
_options = options.Value;
}
/// <summary>
/// 处理发送验证码请求
/// </summary>
/// <param name="request">发送验证码命令</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>发送结果</returns>
public async Task<OperationResult<SendVerificationCodeResponse>> Handle(
SendVerificationCodeCommand request,
CancellationToken cancellationToken)
{
try
{
// 记录开始发送验证码
_logger.LogInformation("开始向邮箱 {Email} 发送验证码", request.Email);
// 调用服务发送验证码
var success = await _emailVerificationService.GenerateAndSendVerificationCodeAsync(request.Email);
if (!success)
{
_logger.LogWarning("向邮箱 {Email} 发送验证码失败", request.Email);
return OperationResult<SendVerificationCodeResponse>.CreateFailure("发送验证码失败");
}
// 记录发送成功
_logger.LogInformation("成功向邮箱 {Email} 发送验证码", request.Email);
// 返回成功响应
return OperationResult<SendVerificationCodeResponse>.CreateSuccess(
new SendVerificationCodeResponse
{
Success = true,
ExpirationMinutes = _options.VerificationCodeExpirationMinutes
});
}
catch (System.Exception ex)
{
// 记录异常
_logger.LogError(ex, "向邮箱 {Email} 发送验证码时发生异常", request.Email);
return OperationResult<SendVerificationCodeResponse>.CreateFailure("发送验证码时发生错误");
}
}
}

38
src/CellularManagement.Application/Features/Auth/Commands/VerifyCode/VerifyCodeCommand.cs

@ -0,0 +1,38 @@
using MediatR;
using CellularManagement.Domain.Common;
namespace CellularManagement.Application.Features.Auth.Commands.VerifyCode;
/// <summary>
/// 验证验证码命令
/// 用于验证用户输入的验证码是否正确
/// </summary>
public sealed record VerifyCodeCommand : IRequest<OperationResult<VerifyCodeResponse>>
{
/// <summary>
/// 邮箱地址
/// </summary>
public string Email { get; init; } = string.Empty;
/// <summary>
/// 用户输入的验证码
/// </summary>
public string Code { get; init; } = string.Empty;
}
/// <summary>
/// 验证验证码响应
/// 包含验证结果信息
/// </summary>
public sealed record VerifyCodeResponse
{
/// <summary>
/// 验证是否成功
/// </summary>
public bool Success { get; init; }
/// <summary>
/// 验证失败的原因(如果验证失败)
/// </summary>
public string? FailureReason { get; init; }
}

78
src/CellularManagement.Application/Features/Auth/Commands/VerifyCode/VerifyCodeCommandHandler.cs

@ -0,0 +1,78 @@
using MediatR;
using Microsoft.Extensions.Logging;
using CellularManagement.Domain.Services;
using CellularManagement.Domain.Common;
using System.Threading;
using System.Threading.Tasks;
namespace CellularManagement.Application.Features.Auth.Commands.VerifyCode;
/// <summary>
/// 验证验证码命令处理器
/// 处理验证码验证的业务逻辑
/// </summary>
public sealed class VerifyCodeCommandHandler : IRequestHandler<VerifyCodeCommand, OperationResult<VerifyCodeResponse>>
{
private readonly IEmailVerificationService _emailVerificationService;
private readonly ILogger<VerifyCodeCommandHandler> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="emailVerificationService">邮箱验证服务</param>
/// <param name="logger">日志记录器</param>
public VerifyCodeCommandHandler(
IEmailVerificationService emailVerificationService,
ILogger<VerifyCodeCommandHandler> logger)
{
_emailVerificationService = emailVerificationService;
_logger = logger;
}
/// <summary>
/// 处理验证验证码请求
/// </summary>
/// <param name="request">验证验证码命令</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>验证结果</returns>
public async Task<OperationResult<VerifyCodeResponse>> Handle(
VerifyCodeCommand request,
CancellationToken cancellationToken)
{
try
{
// 记录开始验证
_logger.LogInformation("开始验证邮箱 {Email} 的验证码", request.Email);
// 调用服务验证验证码
var isValid = await _emailVerificationService.VerifyCodeAsync(request.Email, request.Code);
if (!isValid)
{
_logger.LogWarning("邮箱 {Email} 的验证码验证失败", request.Email);
return OperationResult<VerifyCodeResponse>.CreateSuccess(
new VerifyCodeResponse
{
Success = false,
FailureReason = "验证码无效或已过期"
});
}
// 记录验证成功
_logger.LogInformation("邮箱 {Email} 的验证码验证成功", request.Email);
// 返回成功响应
return OperationResult<VerifyCodeResponse>.CreateSuccess(
new VerifyCodeResponse
{
Success = true
});
}
catch (System.Exception ex)
{
// 记录异常
_logger.LogError(ex, "验证邮箱 {Email} 的验证码时发生异常", request.Email);
return OperationResult<VerifyCodeResponse>.CreateFailure("验证验证码时发生错误");
}
}
}

2
src/CellularManagement.Application/Features/Users/Commands/CreateUser/CreateUserCommandValidator.cs

@ -1,5 +1,5 @@
using FluentValidation;
using CellularManagement.Application.Common;
using CellularManagement.Application.Common.Extensions;
namespace CellularManagement.Application.Features.Users.Commands.CreateUser;

2
src/CellularManagement.Infrastructure/Options/DatabaseOptions.cs → src/CellularManagement.Domain/Options/DatabaseOptions.cs

@ -1,4 +1,4 @@
namespace CellularManagement.Infrastructure.Options;
namespace CellularManagement.Domain.Options;
public class DatabaseOptions
{

2
src/CellularManagement.Infrastructure/Options/EmailOptions.cs → src/CellularManagement.Domain/Options/EmailOptions.cs

@ -1,4 +1,4 @@
namespace CellularManagement.Infrastructure.Options;
namespace CellularManagement.Domain.Options;
/// <summary>
/// 邮箱配置选项

27
src/CellularManagement.Domain/Options/EmailVerificationOptions.cs

@ -0,0 +1,27 @@
namespace CellularManagement.Domain.Options;
/// <summary>
/// 邮箱验证码配置选项
/// </summary>
public class EmailVerificationOptions
{
/// <summary>
/// 配置节点名称
/// </summary>
public const string SectionName = "EmailVerification";
/// <summary>
/// 缓存键前缀
/// </summary>
public string CacheKeyPrefix { get; set; } = "EmailVerification_";
/// <summary>
/// 验证码长度
/// </summary>
public int VerificationCodeLength { get; set; } = 6;
/// <summary>
/// 验证码有效期(分钟)
/// </summary>
public int VerificationCodeExpirationMinutes { get; set; } = 2;
}

134
src/CellularManagement.Infrastructure/Options/JwtOptions.cs → src/CellularManagement.Domain/Options/JwtOptions.cs

@ -1,9 +1,6 @@
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace CellularManagement.Infrastructure.Options;
namespace CellularManagement.Domain.Options;
/// <summary>
/// JWT 配置选项
@ -161,131 +158,4 @@ public class JwtOptions
/// </remarks>
public bool ValidateLifetime { get; set; } = true;
/// <summary>
/// 验证配置
/// </summary>
public void Validate()
{
if (string.IsNullOrEmpty(SecretKey))
{
throw new ArgumentException("JWT密钥不能为空");
}
if (string.IsNullOrEmpty(Issuer))
{
throw new ArgumentException("JWT颁发者不能为空");
}
if (string.IsNullOrEmpty(Audience))
{
throw new ArgumentException("JWT受众不能为空");
}
if (ExpiryMinutes <= 0)
{
throw new ArgumentException("JWT过期时间必须大于0");
}
if (RefreshTokenExpiryDays <= 0)
{
throw new ArgumentException("刷新令牌过期时间必须大于0");
}
if (ClockSkewMinutes < 0)
{
throw new ArgumentException("时钟偏差不能为负数");
}
if (KeyRotationDays <= 0)
{
throw new ArgumentException("密钥轮换间隔必须大于0");
}
if (MinKeyLength < 32)
{
throw new ArgumentException("密钥最小长度必须至少为32字节");
}
// 验证密钥是否为有效的Base64字符串
try
{
var keyBytes = Convert.FromBase64String(SecretKey);
if (keyBytes.Length < MinKeyLength)
{
throw new ArgumentException($"密钥长度必须至少为{MinKeyLength}字节");
}
}
catch (FormatException)
{
throw new ArgumentException("JWT密钥不是有效的Base64字符串");
}
}
}
/// <summary>
/// JwtOptions 扩展方法
/// </summary>
public static class JwtOptionsExtensions
{
/// <summary>
/// 验证密钥强度
/// </summary>
/// <param name="options">JWT配置选项</param>
public static void ValidateKeyStrength(this JwtOptions options)
{
if (string.IsNullOrEmpty(options.SecretKey))
{
throw new ArgumentException("密钥不能为空");
}
// 验证密钥是否为有效的Base64字符串
try
{
var keyBytes = Convert.FromBase64String(options.SecretKey);
if (keyBytes.Length < options.MinKeyLength)
{
throw new ArgumentException($"密钥长度必须至少为 {options.MinKeyLength} 字节");
}
}
catch (FormatException)
{
throw new ArgumentException("密钥不是有效的Base64字符串");
}
// 检查密钥是否包含足够的随机性
var entropy = CalculateEntropy(options.SecretKey);
if (entropy < 3.5) // 3.5 bits per character is considered good
{
throw new ArgumentException("密钥随机性不足");
}
}
/// <summary>
/// 计算字符串的熵
/// </summary>
private static double CalculateEntropy(string input)
{
var frequencies = new Dictionary<char, int>();
foreach (var c in input)
{
if (frequencies.ContainsKey(c))
{
frequencies[c]++;
}
else
{
frequencies[c] = 1;
}
}
var length = input.Length;
var entropy = 0.0;
foreach (var frequency in frequencies.Values)
{
var probability = (double)frequency / length;
entropy -= probability * Math.Log2(probability);
}
return entropy;
}
}
}

22
src/CellularManagement.Domain/Services/JwtValidationService.cs

@ -0,0 +1,22 @@
using CellularManagement.Domain.Options;
namespace CellularManagement.Domain.Services;
/// <summary>
/// JWT 验证服务
/// </summary>
public interface IJwtValidationService
{
/// <summary>
/// 验证 JWT 配置
/// </summary>
/// <param name="options">JWT 配置选项</param>
void ValidateOptions(JwtOptions options);
/// <summary>
/// 验证密钥强度
/// </summary>
/// <param name="secretKey">密钥</param>
/// <param name="minKeyLength">最小密钥长度</param>
void ValidateKeyStrength(string secretKey, int minKeyLength);
}

10
src/CellularManagement.Infrastructure/DependencyInjection.cs

@ -13,6 +13,7 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using CellularManagement.Infrastructure.Configurations;
using CellularManagement.Domain.Services;
using CellularManagement.Domain.Options;
namespace CellularManagement.Infrastructure;
@ -51,6 +52,7 @@ public static class DependencyInjection
// 添加邮箱服务
services.Configure<EmailOptions>(configuration.GetSection(EmailOptions.SectionName));
services.Configure<EmailVerificationOptions>(configuration.GetSection(EmailVerificationOptions.SectionName));
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<IEmailVerificationService, EmailVerificationService>();
@ -111,9 +113,6 @@ public static class DependencyInjection
// 添加内存缓存
services.AddMemoryCache();
// 添加分布式缓存
services.AddDistributedMemoryCache();
// 注册缓存服务
services.AddScoped<ICacheService, CacheService>();
@ -159,6 +158,11 @@ public static class DependencyInjection
services.AddScoped<IJwtProvider, JwtProvider>();
services.AddSingleton<IKeyRotationService, KeyRotationService>();
services.AddHostedService<KeyRotationBackgroundService>();
services.AddSingleton<IJwtValidationService, JwtValidationService>();
// 验证 JWT 配置
var validationService = new JwtValidationService();
validationService.ValidateOptions(jwtOptions);
return services;
}

17
src/CellularManagement.Infrastructure/Options/JwtBearerOptionsSetup.cs

@ -4,6 +4,8 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.Text.Json;
using CellularManagement.Domain.Options;
using CellularManagement.Domain.Services;
namespace CellularManagement.Infrastructure.Options;
@ -14,16 +16,19 @@ public class JwtBearerOptionsSetup : IConfigureOptions<JwtBearerOptions>
{
private readonly JwtOptions _jwtOptions;
private readonly ILogger<JwtBearerOptionsSetup> _logger;
private readonly IJwtValidationService _validationService;
public JwtBearerOptionsSetup(
IOptions<JwtOptions> jwtOptions,
ILogger<JwtBearerOptionsSetup> logger)
ILogger<JwtBearerOptionsSetup> logger,
IJwtValidationService validationService)
{
_jwtOptions = jwtOptions.Value;
_logger = logger;
_validationService = validationService;
// 验证密钥强度
_jwtOptions.ValidateKeyStrength();
// 验证配置
_validationService.ValidateOptions(_jwtOptions);
}
public void Configure(JwtBearerOptions options)
@ -70,15 +75,15 @@ public class JwtBearerOptionsSetup : IConfigureOptions<JwtBearerOptions>
options.TokenValidationParameters = new TokenValidationParameters
{
// 验证颁发者
ValidateIssuer = true,
ValidateIssuer = _jwtOptions.ValidateIssuer,
ValidIssuer = _jwtOptions.Issuer,
// 验证受众
ValidateAudience = true,
ValidateAudience = _jwtOptions.ValidateAudience,
ValidAudience = _jwtOptions.Audience,
// 验证过期时间
ValidateLifetime = true,
ValidateLifetime = _jwtOptions.ValidateLifetime,
// 设置签名密钥
IssuerSigningKey = securityKey,

1
src/CellularManagement.Infrastructure/Services/EmailService.cs

@ -1,6 +1,7 @@
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CellularManagement.Domain.Options;
using CellularManagement.Domain.Services;
using CellularManagement.Infrastructure.Options;
using MailKit.Net.Smtp;

50
src/CellularManagement.Infrastructure/Services/EmailVerificationService.cs

@ -1,8 +1,10 @@
using System;
using System.Threading.Tasks;
using CellularManagement.Domain.Services;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using CellularManagement.Domain.Options;
namespace CellularManagement.Infrastructure.Services;
@ -13,20 +15,23 @@ namespace CellularManagement.Infrastructure.Services;
public class EmailVerificationService : IEmailVerificationService
{
private readonly IEmailService _emailService;
private readonly IDistributedCache _cache;
private const string CacheKeyPrefix = "EmailVerification_";
private const int VerificationCodeLength = 6;
private const int VerificationCodeExpirationMinutes = 5;
private readonly ICacheService _cache;
private readonly EmailVerificationOptions _options;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="emailService">邮箱服务</param>
/// <param name="cache">分布式缓存</param>
public EmailVerificationService(IEmailService emailService, IDistributedCache cache)
/// <param name="cache">缓存服务</param>
/// <param name="options">邮箱验证码配置选项</param>
public EmailVerificationService(
IEmailService emailService,
ICacheService cache,
IOptions<EmailVerificationOptions> options)
{
_emailService = emailService;
_cache = cache;
_options = options.Value;
}
/// <summary>
@ -41,23 +46,23 @@ public class EmailVerificationService : IEmailVerificationService
return false;
}
// 生成6位数字验证码
// 生成验证码
var verificationCode = GenerateVerificationCode();
// 将验证码保存到缓存
var cacheKey = $"{CacheKeyPrefix}{email}";
var cacheValue = JsonSerializer.Serialize(new
var cacheKey = $"{_options.CacheKeyPrefix}{email}";
var cacheValue = new VerificationData
{
Code = verificationCode,
CreatedAt = DateTime.UtcNow
});
};
await _cache.SetStringAsync(
_cache.Set(
cacheKey,
cacheValue,
new DistributedCacheEntryOptions
new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(VerificationCodeExpirationMinutes)
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_options.VerificationCodeExpirationMinutes)
});
// 发送验证码邮件
@ -72,25 +77,22 @@ public class EmailVerificationService : IEmailVerificationService
/// <returns>验证结果</returns>
public async Task<bool> VerifyCodeAsync(string email, string code)
{
var cacheKey = $"{CacheKeyPrefix}{email}";
var cachedValue = await _cache.GetStringAsync(cacheKey);
if (string.IsNullOrEmpty(cachedValue))
var cacheKey = $"{_options.CacheKeyPrefix}{email}";
if (!_cache.TryGetValue<VerificationData>(cacheKey, out var verificationData))
{
return false;
}
var verificationData = JsonSerializer.Deserialize<VerificationData>(cachedValue);
if (verificationData == null)
{
return false;
}
// 验证码是否过期
if (DateTime.UtcNow - verificationData.CreatedAt > TimeSpan.FromMinutes(VerificationCodeExpirationMinutes))
if (DateTime.UtcNow - verificationData.CreatedAt > TimeSpan.FromMinutes(_options.VerificationCodeExpirationMinutes))
{
await _cache.RemoveAsync(cacheKey);
_cache.Remove(cacheKey);
return false;
}
@ -101,7 +103,7 @@ public class EmailVerificationService : IEmailVerificationService
}
// 验证成功后删除缓存
await _cache.RemoveAsync(cacheKey);
_cache.Remove(cacheKey);
return true;
}
@ -113,7 +115,7 @@ public class EmailVerificationService : IEmailVerificationService
var random = new Random();
var code = string.Empty;
for (int i = 0; i < VerificationCodeLength; i++)
for (int i = 0; i < _options.VerificationCodeLength; i++)
{
code += random.Next(0, 10).ToString();
}

14
src/CellularManagement.Infrastructure/Services/JwtProvider.cs

@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Caching.Memory;
using System.Text.Json;
using CellularManagement.Domain.Services;
using CellularManagement.Domain.Options;
namespace CellularManagement.Infrastructure.Services;
@ -30,6 +31,7 @@ public sealed class JwtProvider : IJwtProvider
private readonly ILogger<JwtProvider> _logger;
private readonly IKeyRotationService _keyRotationService;
private readonly ICacheService _cacheService;
private readonly IJwtValidationService _jwtValidationService;
private const string RevokedTokensCacheKey = "RevokedTokens_";
private const string TokenBlacklistCacheKey = "TokenBlacklist_";
private const string KeyCacheKey = "JwtKey_";
@ -41,11 +43,13 @@ public sealed class JwtProvider : IJwtProvider
/// <param name="logger">日志记录器</param>
/// <param name="keyRotationService">密钥轮换服务</param>
/// <param name="cacheService">缓存服务</param>
/// <param name="validationService">配置验证服务</param>
public JwtProvider(
IOptions<JwtOptions> jwtOptions,
ILogger<JwtProvider> logger,
IKeyRotationService keyRotationService,
ICacheService cacheService)
ICacheService cacheService,
IJwtValidationService validationService)
{
_jwtOptions = jwtOptions.Value;
_tokenHandler = new JwtSecurityTokenHandler();
@ -53,8 +57,14 @@ public sealed class JwtProvider : IJwtProvider
_keyRotationService = keyRotationService;
_cacheService = cacheService;
// 验证配置
validationService.ValidateOptions(_jwtOptions);
// 验证密钥强度
_jwtOptions.ValidateKeyStrength();
//_jwtOptions.ValidateKeyStrength();
_jwtValidationService = validationService;
_jwtValidationService.ValidateKeyStrength(_jwtOptions.SecretKey, _jwtOptions.MinKeyLength);
}
/// <inheritdoc />

135
src/CellularManagement.Infrastructure/Services/JwtValidationService.cs

@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CellularManagement.Domain.Options;
using CellularManagement.Domain.Services;
namespace CellularManagement.Infrastructure.Services
{
/// <summary>
/// JWT 验证服务实现
/// </summary>
public class JwtValidationService : IJwtValidationService
{
/// <inheritdoc />
public void ValidateOptions(JwtOptions options)
{
if (string.IsNullOrEmpty(options.SecretKey))
{
throw new ArgumentException("JWT密钥不能为空");
}
if (string.IsNullOrEmpty(options.Issuer))
{
throw new ArgumentException("JWT颁发者不能为空");
}
if (string.IsNullOrEmpty(options.Audience))
{
throw new ArgumentException("JWT受众不能为空");
}
if (options.ExpiryMinutes <= 0)
{
throw new ArgumentException("JWT过期时间必须大于0");
}
if (options.RefreshTokenExpiryDays <= 0)
{
throw new ArgumentException("刷新令牌过期时间必须大于0");
}
if (options.ClockSkewMinutes < 0)
{
throw new ArgumentException("时钟偏差不能为负数");
}
if (options.KeyRotationDays <= 0)
{
throw new ArgumentException("密钥轮换间隔必须大于0");
}
if (options.MinKeyLength < 32)
{
throw new ArgumentException("密钥最小长度必须至少为32字节");
}
// 验证密钥是否为有效的Base64字符串
try
{
var keyBytes = Convert.FromBase64String(options.SecretKey);
if (keyBytes.Length < options.MinKeyLength)
{
throw new ArgumentException($"密钥长度必须至少为{options.MinKeyLength}字节");
}
}
catch (FormatException)
{
throw new ArgumentException("JWT密钥不是有效的Base64字符串");
}
}
/// <inheritdoc />
public void ValidateKeyStrength(string secretKey, int minKeyLength)
{
if (string.IsNullOrEmpty(secretKey))
{
throw new ArgumentException("密钥不能为空");
}
// 验证密钥是否为有效的Base64字符串
try
{
var keyBytes = Convert.FromBase64String(secretKey);
if (keyBytes.Length < minKeyLength)
{
throw new ArgumentException($"密钥长度必须至少为 {minKeyLength} 字节");
}
}
catch (FormatException)
{
throw new ArgumentException("密钥不是有效的Base64字符串");
}
// 检查密钥是否包含足够的随机性
var entropy = CalculateEntropy(secretKey);
if (entropy < 3.5) // 3.5 bits per character is considered good
{
throw new ArgumentException("密钥随机性不足");
}
}
/// <summary>
/// 计算字符串熵值
/// </summary>
private static double CalculateEntropy(string input)
{
var charCounts = new Dictionary<char, int>();
foreach (var c in input)
{
if (charCounts.ContainsKey(c))
{
charCounts[c]++;
}
else
{
charCounts[c] = 1;
}
}
var length = input.Length;
var entropy = 0.0;
foreach (var count in charCounts.Values)
{
var probability = (double)count / length;
entropy -= probability * Math.Log2(probability);
}
return entropy;
}
}
}

9
src/CellularManagement.Infrastructure/Services/KeyRotationService.cs

@ -6,6 +6,7 @@ using CellularManagement.Infrastructure.Options;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
using CellularManagement.Domain.Services;
using CellularManagement.Domain.Options;
namespace CellularManagement.Infrastructure.Services;
@ -23,13 +24,19 @@ public sealed class KeyRotationService : IKeyRotationService
/// <summary>
/// 构造函数
/// </summary>
public KeyRotationService(IOptions<JwtOptions> jwtOptions, ILogger<KeyRotationService> logger)
public KeyRotationService(
IOptions<JwtOptions> jwtOptions,
ILogger<KeyRotationService> logger,
IJwtValidationService validationService)
{
_jwtOptions = jwtOptions.Value;
_logger = logger;
_currentKey = _jwtOptions.SecretKey;
_nextKey = GenerateNewKey();
_lastRotationTime = DateTime.UtcNow;
// 验证配置
validationService.ValidateOptions(_jwtOptions);
}
/// <inheritdoc />

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

@ -14,6 +14,8 @@ using CellularManagement.Presentation.Abstractions;
using CellularManagement.Application.Features.Auth.Commands.Login;
using Swashbuckle.AspNetCore.Filters;
using CellularManagement.Domain.Common;
using CellularManagement.Application.Features.Auth.Commands.SendVerificationCode;
using CellularManagement.Application.Features.Auth.Commands.VerifyCode;
namespace CellularManagement.Presentation.Controllers;
@ -21,6 +23,8 @@ namespace CellularManagement.Presentation.Controllers;
/// 认证控制器
/// 提供用户认证相关的 API 接口,包括登录和注册功能
/// </summary>
[Route("api/auth")]
[ApiController]
public class AuthController : ApiController
{
private readonly ILogger<AuthController> _logger;
@ -67,7 +71,7 @@ public class AuthController : ApiController
/// <response code="200">登录成功,返回用户信息和令牌</response>
/// <response code="400">登录失败,返回错误信息</response>
/// <response code="429">登录尝试次数过多</response>
[HttpPost()]
[HttpPost("login")]
[SwaggerRequestExample(typeof(LoginRequest), typeof(LoginRequestExample))]
[ProducesResponseType(typeof(OperationResult<AuthenticateUserResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<AuthenticateUserResponse>), StatusCodes.Status400BadRequest)]
@ -142,7 +146,7 @@ public class AuthController : ApiController
/// </returns>
/// <response code="200">注册成功,返回用户ID</response>
/// <response code="400">注册失败,返回错误信息</response>
[HttpPost()]
[HttpPost("register")]
[ProducesResponseType(typeof(OperationResult<RegisterUserResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<RegisterUserResponse>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OperationResult<RegisterUserResponse>>> Register([FromBody] RegisterUserCommand command)
@ -193,7 +197,7 @@ public class AuthController : ApiController
/// </returns>
/// <response code="200">刷新成功,返回用户信息和新的令牌</response>
/// <response code="400">刷新失败,返回错误信息</response>
[HttpPost()]
[HttpPost("refresh-token")]
[ProducesResponseType(typeof(OperationResult<AuthenticateUserResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<AuthenticateUserResponse>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OperationResult<AuthenticateUserResponse>>> RefreshToken([FromBody] RefreshTokenCommand command)
@ -221,4 +225,109 @@ public class AuthController : ApiController
OperationResult<AuthenticateUserResponse>.CreateFailure("系统错误,请稍后重试"));
}
}
/// <summary>
/// 发送验证码
/// </summary>
/// <remarks>
/// 示例请求:
///
/// POST /api/auth/verification-codes
/// {
/// "email": "user@example.com"
/// }
///
/// </remarks>
/// <param name="command">发送验证码命令</param>
/// <returns>
/// 发送结果,包含:
/// - 成功:返回验证码有效期
/// - 失败:返回错误信息
/// </returns>
/// <response code="200">发送成功,返回验证码有效期</response>
/// <response code="400">发送失败,返回错误信息</response>
[HttpPost("verification-codes")]
[ProducesResponseType(typeof(OperationResult<SendVerificationCodeResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<SendVerificationCodeResponse>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OperationResult<SendVerificationCodeResponse>>> SendVerificationCode(
[FromBody] SendVerificationCodeCommand command)
{
try
{
// 执行发送验证码
var result = await mediator.Send(command);
if (result.IsSuccess)
{
_logger.LogInformation("成功向邮箱 {Email} 发送验证码", command.Email);
}
else
{
_logger.LogWarning("向邮箱 {Email} 发送验证码失败: {Error}",
command.Email,
result.ErrorMessages?.FirstOrDefault());
}
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "向邮箱 {Email} 发送验证码时发生异常", command.Email);
return StatusCode(StatusCodes.Status500InternalServerError,
OperationResult<SendVerificationCodeResponse>.CreateFailure("系统错误,请稍后重试"));
}
}
/// <summary>
/// 校验验证码
/// </summary>
/// <remarks>
/// 示例请求:
///
/// POST /api/auth/verification-codes/verify
/// {
/// "email": "user@example.com",
/// "code": "123456"
/// }
///
/// </remarks>
/// <param name="command">验证验证码命令</param>
/// <returns>
/// 验证结果,包含:
/// - 成功:返回验证成功信息
/// - 失败:返回错误信息
/// </returns>
/// <response code="200">验证成功,返回验证结果</response>
/// <response code="400">验证失败,返回错误信息</response>
[HttpPost("verification-codes/verify")]
[ProducesResponseType(typeof(OperationResult<VerifyCodeResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<VerifyCodeResponse>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OperationResult<VerifyCodeResponse>>> VerifyCode(
[FromBody] VerifyCodeCommand command)
{
try
{
// 执行验证码验证
var result = await mediator.Send(command);
if (result.IsSuccess)
{
_logger.LogInformation("邮箱 {Email} 的验证码验证成功", command.Email);
}
else
{
_logger.LogWarning("邮箱 {Email} 的验证码验证失败: {Error}",
command.Email,
result.ErrorMessages?.FirstOrDefault());
}
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "验证邮箱 {Email} 的验证码时发生异常", command.Email);
return StatusCode(StatusCodes.Status500InternalServerError,
OperationResult<VerifyCodeResponse>.CreateFailure("系统错误,请稍后重试"));
}
}
}

2
src/CellularManagement.Presentation/Controllers/PermissionsController.cs

@ -14,6 +14,8 @@ namespace CellularManagement.Presentation.Controllers;
/// 权限管理控制器
/// 提供权限管理相关的 API 接口,包括创建、删除和查询权限功能
/// </summary>
[Route("api/permissions")]
[ApiController]
[Authorize(Roles = "Admin")] // 只有管理员可以访问
public class PermissionsController : ApiController
{

2
src/CellularManagement.Presentation/Controllers/RolePermissionController.cs

@ -15,6 +15,8 @@ namespace CellularManagement.Presentation.Controllers;
/// 角色权限控制器
/// 提供角色权限管理相关的 API 接口,包括添加、删除和查询角色权限功能
/// </summary>
[Route("api/role-permissions")]
[ApiController]
public class RolePermissionController : ApiController
{
private readonly ILogger<RolePermissionController> _logger;

2
src/CellularManagement.Presentation/Controllers/RolesController.cs

@ -18,6 +18,8 @@ namespace CellularManagement.Presentation.Controllers;
/// 提供角色管理相关的 API 接口,包括创建、删除和查询角色功能
/// </summary>
//[Authorize(Roles = "Admin")] // 只有管理员可以访问
[Route("api/roles")]
[ApiController]
public class RolesController : ApiController
{
private readonly ILogger<RolesController> _logger;

4
src/CellularManagement.Presentation/Controllers/UsersController.cs

@ -19,6 +19,8 @@ namespace CellularManagement.Presentation.Controllers;
/// 用户管理控制器
/// 提供用户管理相关的 API 接口,包括用户信息的查询、更新和删除等功能
/// </summary>
[Route("api/users")]
[ApiController]
[Authorize]
public class UsersController : ApiController
{
@ -318,7 +320,7 @@ public class UsersController : ApiController
/// <response code="200">查询成功,返回用户信息</response>
/// <response code="401">未授权,用户未登录</response>
/// <response code="404">用户不存在</response>
[HttpGet()]
[HttpGet("current")]
[ProducesResponseType(typeof(OperationResult<GetUserByIdResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<object>), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(OperationResult<object>), StatusCodes.Status404NotFound)]

3
src/CellularManagement.WebAPI/Program.cs

@ -19,6 +19,7 @@ using CellularManagement.Application.Features.Auth.Commands.Login;
using Swashbuckle.AspNetCore.Filters;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using CellularManagement.Domain.Options;
// 创建 Web 应用程序构建器
var builder = WebApplication.CreateBuilder(args);
@ -41,7 +42,7 @@ builder.Services.AddInfrastructure(builder.Configuration);
// 注册应用层服务
// 包括命令/查询处理、验证等服务
builder.Services.AddApplication();
builder.Services.AddApplication(builder.Configuration);
// 注册表现层服务
// 包括控制器、视图、中间件等表现层相关服务

13
src/CellularManagement.WebAPI/appsettings.json

@ -34,5 +34,18 @@
"DefaultUserRole": "User",
"AccessTokenExpirationMinutes": 60,
"RefreshTokenExpirationDays": 7
},
"Email": {
"SmtpServer": "smtp.qq.com",
"SmtpPort": 465,
"FromEmail": "295172551@qq.com",
"FromName": "系统通知",
"Password": "rugqfbgcnpkdbhee",
"EnableSsl": true
},
"EmailVerification": {
"CacheKeyPrefix": "EmailVerification_",
"VerificationCodeLength": 6,
"VerificationCodeExpirationMinutes": 2
}
}

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

@ -20,7 +20,7 @@ class ApiError extends Error {
export const apiService: ApiService = {
login: async (request: LoginRequest): Promise<OperationResult<LoginResponse>> => {
try {
const response = await httpClient.post<LoginResponse>('/Auth/Login', request);
const response = await httpClient.post<LoginResponse>('/auth/login', request);
return {
success: true,
data: response.data,
@ -36,7 +36,7 @@ export const apiService: ApiService = {
register: async (request: RegisterRequest): Promise<OperationResult<void>> => {
try {
await httpClient.post('/Auth/Register', request);
await httpClient.post('/auth/register', request);
return {
success: true,
message: AUTH_CONSTANTS.MESSAGES.REGISTER_SUCCESS
@ -51,7 +51,7 @@ export const apiService: ApiService = {
refreshToken: async (refreshToken: string): Promise<OperationResult<LoginResponse>> => {
try {
const response = await httpClient.post<LoginResponse>('/Auth/RefreshToken', { refreshToken });
const response = await httpClient.post<LoginResponse>('/auth/refresh-token', { refreshToken });
return {
success: true,
data: response.data,
@ -67,7 +67,7 @@ export const apiService: ApiService = {
logout: async (): Promise<OperationResult<void>> => {
try {
await httpClient.post('/Auth/Logout');
await httpClient.post('/auth/logout');
return {
success: true,
message: AUTH_CONSTANTS.MESSAGES.LOGOUT_SUCCESS
@ -82,7 +82,7 @@ export const apiService: ApiService = {
getCurrentUser: async (): Promise<OperationResult<User>> => {
try {
const response = await httpClient.get<User>('/Users/CurrentUser');
const response = await httpClient.get<User>('/users/current');
return {
success: true,
data: response.data,

8
src/CellularManagement.WebUI/src/services/roleService.ts

@ -41,7 +41,7 @@ export const roleService: RoleService = {
PageSize: params.pageSize ?? 10,
RoleName: params.roleName ?? undefined
};
const response = await httpClient.get<{ data: GetAllRolesResponse }>('/Roles/GetAllRoles', { params: mappedParams });
const response = await httpClient.get<{ data: GetAllRolesResponse }>('/roles', { params: mappedParams });
// 兼容 response.data 可能为 { roles, ... } 或 { data: { roles, ... } }
const resultData = (response.data && 'roles' in response.data) ? response.data : response.data?.data;
return {
@ -59,7 +59,7 @@ export const roleService: RoleService = {
getRole: async (roleId: string): Promise<OperationResult<Role>> => {
try {
const response = await httpClient.get<Role>(`/Roles/GetRole/${roleId}`);
const response = await httpClient.get<Role>(`/roles/${roleId}`);
return {
success: true,
data: response.data || undefined,
@ -75,7 +75,7 @@ export const roleService: RoleService = {
createRole: async (data: CreateRoleRequest): Promise<OperationResult<Role>> => {
try {
const response = await httpClient.post<Role>('/Roles/CreateRole', data);
const response = await httpClient.post<Role>('/roles', data);
return {
success: true,
data: response.data || undefined,
@ -91,7 +91,7 @@ export const roleService: RoleService = {
deleteRole: async (roleId: string): Promise<OperationResult<void>> => {
try {
await httpClient.delete(`/Roles/DeleteRole/${roleId}`);
await httpClient.delete(`/roles/${roleId}`);
return {
success: true,
message: '删除角色成功'

Loading…
Cancel
Save