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 |
|||
{ |
@ -1,4 +1,4 @@ |
|||
namespace CellularManagement.Infrastructure.Options; |
|||
namespace CellularManagement.Domain.Options; |
|||
|
|||
/// <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