32 changed files with 697 additions and 217 deletions
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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; } |
||||
|
} |
||||
@ -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("发送验证码时发生错误"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
@ -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("验证验证码时发生错误"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,4 +1,4 @@ |
|||||
namespace CellularManagement.Infrastructure.Options; |
namespace CellularManagement.Domain.Options; |
||||
|
|
||||
public class DatabaseOptions |
public class DatabaseOptions |
||||
{ |
{ |
||||
@ -1,4 +1,4 @@ |
|||||
namespace CellularManagement.Infrastructure.Options; |
namespace CellularManagement.Domain.Options; |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// 邮箱配置选项
|
/// 邮箱配置选项
|
||||
@ -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; |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue