Browse Source

feat: 完善JWT认证和用户管理功能

web
hyh 3 months ago
parent
commit
2fe019001f
  1. 211
      docs/JWT-Implementation-Guide.md
  2. 217
      docs/JWT-Service-Registration-Guide.md
  3. 5
      src/CellularManagement.Application/CellularManagement.Application.csproj
  4. 23
      src/CellularManagement.Application/CellularManagement.Application.csproj.Backup.tmp
  5. 2
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommand.cs
  6. 14
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandHandler.cs
  7. 6
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandValidator.cs
  8. 21
      src/CellularManagement.Application/Features/Auth/Commands/Login/LoginRequest.cs
  9. 45
      src/CellularManagement.Application/Features/Auth/Commands/Login/LoginRequestExample.cs
  10. 2
      src/CellularManagement.Infrastructure/Context/AppDbContext.cs
  11. 47
      src/CellularManagement.Infrastructure/DependencyInjection.cs
  12. 12
      src/CellularManagement.Infrastructure/Migrations/20250514055354_InitialCreate.Designer.cs
  13. 2
      src/CellularManagement.Infrastructure/Migrations/20250514055354_InitialCreate.cs
  14. 10
      src/CellularManagement.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
  15. 202
      src/CellularManagement.Infrastructure/Options/JwtBearerOptionsSetup.cs
  16. 148
      src/CellularManagement.Infrastructure/Options/JwtOptions.cs
  17. 134
      src/CellularManagement.Infrastructure/Services/JwtProvider.cs
  18. 94
      src/CellularManagement.Infrastructure/Services/KeyRotationService.cs
  19. 17
      src/CellularManagement.Presentation/Controllers/AuthController.cs
  20. 57
      src/CellularManagement.Presentation/Controllers/UsersController.cs
  21. 4
      src/CellularManagement.WebAPI/CellularManagement.WebAPI.csproj
  22. 88
      src/CellularManagement.WebAPI/Program.cs
  23. 2
      src/CellularManagement.WebAPI/appsettings.json
  24. 2
      src/CellularManagement.WebSocket/Connection/WebSocketConnectionManager.cs

211
docs/JWT-Implementation-Guide.md

@ -0,0 +1,211 @@
# JWT 实现指南
## 1. JwtOptions(配置类)
### 1.1 主要职责
- 存储和管理 JWT 相关的所有配置项
- 提供配置验证功能
- 支持从配置文件加载配置
### 1.2 关键配置项
```csharp
public class JwtOptions
{
public string SecretKey { get; set; } // JWT 密钥
public string Issuer { get; set; } // 颁发者
public string Audience { get; set; } // 受众
public int ExpiryMinutes { get; set; } // 访问令牌过期时间
public int RefreshTokenExpiryDays { get; set; } // 刷新令牌过期时间
public int ClockSkewMinutes { get; set; } // 时钟偏差
public int KeyRotationDays { get; set; } // 密钥轮换间隔
public int MinKeyLength { get; set; } // 密钥最小长度
}
```
### 1.3 配置验证
- 验证所有必需字段不为空
- 验证数值字段的有效性
- 验证密钥格式和长度
## 2. JwtOptionsExtensions(扩展方法类)
### 2.1 主要职责
- 提供 JwtOptions 的扩展功能
- 实现密钥强度验证
- 提供熵值计算功能
### 2.2 关键方法
```csharp
public static class JwtOptionsExtensions
{
// 验证密钥强度
public static void ValidateKeyStrength(this JwtOptions options)
// 计算字符串熵值
private static double CalculateEntropy(string input)
}
```
### 2.3 验证标准
- 密钥不能为空
- 密钥必须是有效的 Base64 字符串
- 密钥长度必须满足最小长度要求
- 密钥熵值必须大于 3.5(确保足够的随机性)
## 3. KeyRotationService(密钥管理服务)
### 3.1 主要职责
- 管理 JWT 密钥的生命周期
- 实现密钥自动轮换
- 提供密钥生成和验证功能
### 3.2 关键功能
- 密钥初始化
- 密钥轮换
- 密钥强度验证
- 密钥缓存管理
### 3.3 安全特性
- 定期自动轮换密钥
- 支持密钥预热
- 防止密钥泄露
- 密钥强度保证
## 4. JwtProvider(JWT 令牌提供者)
### 4.1 主要职责
- 生成 JWT 访问令牌和刷新令牌
- 验证 JWT 令牌
- 管理令牌黑名单
- 提供令牌解析功能
### 4.2 关键方法
```csharp
public interface IJwtProvider
{
string GenerateAccessToken(IEnumerable<Claim> claims);
string GenerateRefreshToken(IEnumerable<Claim> claims);
bool ValidateToken(string token);
void RevokeToken(string token);
void AddToBlacklist(string token);
IEnumerable<Claim> GetClaimsFromToken(string token);
}
```
### 4.3 安全特性
- 令牌撤销机制
- 令牌黑名单
- 完整的令牌验证
- 支持自定义声明
## 5. JwtBearerOptionsSetup(JWT Bearer 认证配置)
### 5.1 主要职责
- 配置 ASP.NET Core JWT Bearer 认证
- 设置令牌验证参数
- 配置认证事件处理
### 5.2 关键配置
```csharp
public class JwtBearerOptionsSetup : IConfigureOptions<JwtBearerOptions>
{
public void Configure(JwtBearerOptions options)
{
// 配置令牌验证参数
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = _jwtOptions.Issuer,
ValidateAudience = true,
ValidAudience = _jwtOptions.Audience,
ValidateLifetime = true,
// ... 其他配置
};
}
}
```
### 5.3 安全特性
- 强制使用 HTTPS
- 完整的令牌验证
- 详细的日志记录
- 异常处理机制
## 安全最佳实践
1. **密钥管理**
- 使用足够长的随机密钥
- 定期轮换密钥
- 使用安全的密钥存储机制
2. **令牌安全**
- 设置合理的过期时间
- 实现令牌撤销机制
- 使用令牌黑名单
3. **传输安全**
- 强制使用 HTTPS
- 验证令牌签名
- 验证令牌来源
4. **配置安全**
- 验证所有配置项
- 使用强类型配置
- 避免硬编码敏感信息
## 使用示例
### 1. 配置 JWT 选项
```csharp
services.Configure<JwtOptions>(configuration.GetSection(JwtOptions.SectionName));
```
### 2. 注册服务
```csharp
services.AddScoped<IJwtProvider, JwtProvider>();
services.AddScoped<IKeyRotationService, KeyRotationService>();
services.AddSingleton<IConfigureOptions<JwtBearerOptions>, JwtBearerOptionsSetup>();
```
### 3. 使用 JWT 提供者
```csharp
public class AuthController : ControllerBase
{
private readonly IJwtProvider _jwtProvider;
public AuthController(IJwtProvider jwtProvider)
{
_jwtProvider = jwtProvider;
}
public IActionResult Login(LoginRequest request)
{
// 验证用户
var claims = GetUserClaims(user);
var token = _jwtProvider.GenerateAccessToken(claims);
return Ok(new { token });
}
}
```
## 注意事项
1. **密钥管理**
- 不要在代码中硬编码密钥
- 使用环境变量或密钥管理服务
- 定期轮换密钥
2. **令牌配置**
- 设置合理的过期时间
- 启用所有安全验证
- 使用 HTTPS
3. **错误处理**
- 实现完整的错误处理
- 记录详细的日志
- 返回适当的错误信息
4. **性能考虑**
- 使用缓存机制
- 优化令牌验证
- 控制令牌大小

217
docs/JWT-Service-Registration-Guide.md

@ -0,0 +1,217 @@
# JWT 服务注册指南
## 服务注册概览
JWT 相关的服务注册主要分布在两个位置:
1. `Program.cs` - Web API 层的服务注册
2. `DependencyInjection.cs` - 基础设施层的服务注册
## 1. 基础设施层注册 (DependencyInjection.cs)
### 1.1 JWT 配置注册
```csharp
// 配置 JWT 选项
services.Configure<JwtOptions>(configuration.GetSection(JwtOptions.SectionName));
services.AddSingleton<IConfigureOptions<JwtBearerOptions>, JwtBearerOptionsSetup>();
services.AddScoped<IJwtProvider, JwtProvider>();
```
说明:
- `Configure<JwtOptions>`: 从配置文件加载 JWT 配置
- `JwtBearerOptionsSetup`: 配置 JWT Bearer 认证选项
- `JwtProvider`: 实现 JWT 令牌的生成和验证
### 1.2 密钥管理服务注册
```csharp
// 注册密钥轮换服务
services.AddSingleton<IKeyRotationService, KeyRotationService>();
services.AddHostedService<KeyRotationBackgroundService>();
```
说明:
- `KeyRotationService`: 管理 JWT 密钥的生命周期
- `KeyRotationBackgroundService`: 后台服务,定期执行密钥轮换
### 1.3 认证服务注册
```csharp
// 配置JWT认证
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer();
services.AddAuthorization();
```
说明:
- 设置默认认证方案为 JWT Bearer
- 启用授权服务
## 2. Web API 层注册 (Program.cs)
### 2.1 JWT 配置注册
```csharp
// 配置JWT认证
builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection("JwtOptions"));
builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>, JwtBearerOptionsSetup>();
```
### 2.2 认证服务注册
```csharp
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer();
```
### 2.3 Swagger 配置
```csharp
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
```
## 3. 服务注册优化建议
### 3.1 避免重复注册
目前存在重复注册的问题:
1. JWT 配置在两个地方都进行了注册
2. 认证服务在两个地方都进行了配置
建议优化方案:
```csharp
// 在 DependencyInjection.cs 中统一注册
public static IServiceCollection AddJwtServices(
this IServiceCollection services,
IConfiguration configuration)
{
// 配置 JWT 选项
services.Configure<JwtOptions>(configuration.GetSection(JwtOptions.SectionName));
// 注册 JWT 服务
services.AddSingleton<IConfigureOptions<JwtBearerOptions>, JwtBearerOptionsSetup>();
services.AddScoped<IJwtProvider, JwtProvider>();
services.AddSingleton<IKeyRotationService, KeyRotationService>();
services.AddHostedService<KeyRotationBackgroundService>();
// 配置认证
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer();
services.AddAuthorization();
return services;
}
```
### 3.2 配置文件结构
建议的 JWT 配置结构:
```json
{
"JwtOptions": {
"SecretKey": "your-secret-key",
"Issuer": "your-issuer",
"Audience": "your-audience",
"ExpiryMinutes": 15,
"RefreshTokenExpiryDays": 7,
"ClockSkewMinutes": 5,
"KeyRotationDays": 30,
"MinKeyLength": 64
}
}
```
## 4. 中间件配置
### 4.1 认证中间件
```csharp
// 启用认证中间件
app.UseAuthentication();
// 启用授权中间件
app.UseAuthorization();
```
### 4.2 HTTPS 重定向
```csharp
// 启用 HTTPS 重定向
app.UseHttpsRedirection();
```
## 5. 使用建议
1. **配置管理**
- 使用强类型配置
- 集中管理配置项
- 避免硬编码敏感信息
2. **服务注册**
- 使用扩展方法组织服务注册
- 避免重复注册
- 遵循依赖注入最佳实践
3. **安全配置**
- 启用 HTTPS
- 配置适当的 CORS 策略
- 实现完整的认证和授权
4. **开发体验**
- 配置 Swagger 文档
- 提供详细的错误信息
- 实现适当的日志记录
## 6. 常见问题
1. **配置加载失败**
- 检查配置文件路径
- 验证配置节点名称
- 确保配置值格式正确
2. **认证失败**
- 检查令牌格式
- 验证密钥配置
- 确认过期时间设置
3. **密钥轮换问题**
- 检查轮换间隔设置
- 验证密钥生成逻辑
- 确保缓存正确更新
4. **性能问题**
- 优化令牌验证
- 使用适当的缓存策略
- 控制令牌大小

5
src/CellularManagement.Application/CellularManagement.Application.csproj

@ -1,4 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\CellularManagement.Domain\CellularManagement.Domain.csproj" />
@ -10,6 +11,8 @@
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.8" />
</ItemGroup>
<PropertyGroup>

23
src/CellularManagement.Application/CellularManagement.Application.csproj.Backup.tmp

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\CellularManagement.Domain\CellularManagement.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Examples" Version="2.9.0" />
<PackageReference Include="Swashbuckle.Examples" Version="4.1.0" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

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

@ -9,7 +9,7 @@ public sealed record AuthenticateUserCommand(
/// <summary>
/// 用户名或邮箱
/// </summary>
string UserNameOrEmail,
string UserName,
/// <summary>
/// 密码

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

@ -46,20 +46,20 @@ public sealed class AuthenticateUserCommandHandler : IRequestHandler<Authenticat
{
// 先尝试通过邮箱查找用户
AppUser? user = null;
if (request.UserNameOrEmail.IsValidEmail())
if (request.UserName.IsValidEmail())
{
user = await _userManager.FindByEmailAsync(request.UserNameOrEmail);
user = await _userManager.FindByEmailAsync(request.UserName);
}
// 如果通过邮箱没找到用户,则尝试通过用户名查找
if (user == null)
{
user = await _userManager.FindByNameAsync(request.UserNameOrEmail);
user = await _userManager.FindByNameAsync(request.UserName);
}
if (user == null)
{
_logger.LogWarning("用户 {UserNameOrEmail} 不存在", request.UserNameOrEmail);
_logger.LogWarning("用户 {UserName} 不存在", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("用户名或密码错误");
}
@ -67,7 +67,7 @@ public sealed class AuthenticateUserCommandHandler : IRequestHandler<Authenticat
var isValidPassword = await _userManager.CheckPasswordAsync(user, request.Password);
if (!isValidPassword)
{
_logger.LogWarning("用户 {UserNameOrEmail} 密码错误", request.UserNameOrEmail);
_logger.LogWarning("用户 {UserName} 密码错误", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("用户名或密码错误");
}
@ -102,7 +102,7 @@ public sealed class AuthenticateUserCommandHandler : IRequestHandler<Authenticat
user.PhoneNumber,
roles);
_logger.LogInformation("用户 {UserNameOrEmail} 认证成功", request.UserNameOrEmail);
_logger.LogInformation("用户 {UserName} 认证成功", request.UserName);
// 返回认证结果
return OperationResult<AuthenticateUserResponse>.CreateSuccess(
@ -110,7 +110,7 @@ public sealed class AuthenticateUserCommandHandler : IRequestHandler<Authenticat
}
catch (Exception ex)
{
_logger.LogError(ex, "用户 {UserNameOrEmail} 认证失败", request.UserNameOrEmail);
_logger.LogError(ex, "用户 {UserName} 认证失败", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("认证失败,请稍后重试");
}
}

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

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

21
src/CellularManagement.Application/Features/Auth/Commands/Login/LoginRequest.cs

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
namespace CellularManagement.Application.Features.Auth.Commands.Login;
/// <summary>
/// 登录请求
/// </summary>
public class LoginRequest
{
/// <summary>
/// 用户名
/// </summary>
[Required(ErrorMessage = "用户名不能为空")]
public string UserName { get; set; } = string.Empty;
/// <summary>
/// 密码
/// </summary>
[Required(ErrorMessage = "密码不能为空")]
public string Password { get; set; } = string.Empty;
}

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

@ -0,0 +1,45 @@
using Swashbuckle.AspNetCore.Filters;
namespace CellularManagement.Application.Features.Auth.Commands.Login;
/// <summary>
/// 登录请求示例
/// </summary>
public class LoginRequestExample : IExamplesProvider<LoginRequest>
{
/// <summary>
/// 获取示例
/// </summary>
/// <returns>登录请求示例</returns>
public LoginRequest GetExamples()
{
return new LoginRequest
{
UserName = "zhangsan",
Password = "P@ssw0rd!"
};
}
}
/// <summary>
/// 登录请求示例提供者
/// </summary>
public class LoginRequestExamples : IMultipleExamplesProvider<LoginRequest>
{
/// <summary>
/// 获取示例
/// </summary>
/// <returns>登录请求示例集合</returns>
public IEnumerable<SwaggerExample<LoginRequest>> GetExamples()
{
yield return SwaggerExample.Create(
"默认用户",
"使用默认测试账号",
new LoginRequest
{
UserName = "zhangsan",
Password = "P@ssw0rd!"
}
);
}
}

2
src/CellularManagement.Infrastructure/Context/AppDbContext.cs

@ -65,6 +65,8 @@ public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
entity.HasKey(p => p.Id);
entity.Property(p => p.Name).IsRequired().HasMaxLength(50);
entity.Property(p => p.Description).HasMaxLength(200);
entity.Property(p => p.Code).IsRequired().HasMaxLength(50);
entity.Property(p => p.Type).IsRequired().HasMaxLength(50);
entity.Property(p => p.CreatedAt).IsRequired();
});

47
src/CellularManagement.Infrastructure/DependencyInjection.cs

@ -9,6 +9,9 @@ using CellularManagement.Domain.Repositories;
using CellularManagement.Application.Services;
using CellularManagement.Infrastructure.Services;
using Scrutor;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace CellularManagement.Infrastructure;
@ -42,9 +45,8 @@ public static class DependencyInjection
"Database options not configured");
}
// 配置 JWT 选项
services.Configure<JwtOptions>(configuration.GetSection(JwtOptions.SectionName));
services.ConfigureOptions<JwtBearerOptionsSetup>();
// 添加 JWT 服务
services.AddJwtServices(configuration);
// 配置数据库选项
services.Configure<DatabaseOptions>(configuration.GetSection(DatabaseOptions.SectionName));
@ -57,10 +59,6 @@ public static class DependencyInjection
"Database connection string not configured");
}
// 注册密钥轮换服务
services.AddSingleton<IKeyRotationService, KeyRotationService>();
services.AddHostedService<KeyRotationBackgroundService>();
// 配置数据库上下文
services.AddDbContext<AppDbContext>(options =>
{
@ -88,13 +86,6 @@ public static class DependencyInjection
}
});
// 配置身份认证
services
.AddAuthentication()
.AddJwtBearer();
services
.AddAuthorization();
// 配置身份认证服务
services
.AddIdentityCore<AppUser>()
@ -134,4 +125,32 @@ public static class DependencyInjection
return services;
}
/// <summary>
/// 添加 JWT 服务
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configuration">配置</param>
/// <returns>服务集合</returns>
private static IServiceCollection AddJwtServices(
this IServiceCollection services,
IConfiguration configuration)
{
// 配置 JWT 选项
var jwtOptions = configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>();
if (jwtOptions == null)
{
throw new InvalidOperationException("JWT配置未找到");
}
services.Configure<JwtOptions>(configuration.GetSection(JwtOptions.SectionName));
// 注册 JWT 服务
services.AddSingleton<IConfigureOptions<JwtBearerOptions>, JwtBearerOptionsSetup>();
services.AddScoped<IJwtProvider, JwtProvider>();
services.AddSingleton<IKeyRotationService, KeyRotationService>();
services.AddHostedService<KeyRotationBackgroundService>();
return services;
}
}

12
src/CellularManagement.Infrastructure/Migrations/20250509092443_InitialCreate.Designer.cs → src/CellularManagement.Infrastructure/Migrations/20250514055354_InitialCreate.Designer.cs

@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace CellularManagement.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250509092443_InitialCreate")]
[Migration("20250514055354_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
@ -171,6 +171,11 @@ namespace CellularManagement.Infrastructure.Migrations
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
@ -183,6 +188,11 @@ namespace CellularManagement.Infrastructure.Migrations
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Permissions", (string)null);

2
src/CellularManagement.Infrastructure/Migrations/20250509092443_InitialCreate.cs → src/CellularManagement.Infrastructure/Migrations/20250514055354_InitialCreate.cs

@ -17,7 +17,9 @@ namespace CellularManagement.Infrastructure.Migrations
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
Code = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
Description = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Type = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>

10
src/CellularManagement.Infrastructure/Migrations/AppDbContextModelSnapshot.cs

@ -168,6 +168,11 @@ namespace CellularManagement.Infrastructure.Migrations
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
@ -180,6 +185,11 @@ namespace CellularManagement.Infrastructure.Migrations
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Permissions", (string)null);

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

@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.Text.Json;
namespace CellularManagement.Infrastructure.Options;
@ -27,86 +28,161 @@ public class JwtBearerOptionsSetup : IConfigureOptions<JwtBearerOptions>
public void Configure(JwtBearerOptions options)
{
// 创建对称安全密钥
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecretKey));
// 配置令牌验证参数
options.TokenValidationParameters = new TokenValidationParameters
try
{
// 验证颁发者
ValidateIssuer = true,
ValidIssuer = _jwtOptions.Issuer,
_logger.LogInformation("开始配置JWT Bearer选项");
// 记录JWT配置信息
_logger.LogInformation("JWT配置信息: {Config}", JsonSerializer.Serialize(new
{
SecretKey = _jwtOptions.SecretKey?.Length > 0 ? "已配置" : "未配置",
Issuer = _jwtOptions.Issuer,
Audience = _jwtOptions.Audience,
ExpiryMinutes = _jwtOptions.ExpiryMinutes,
ValidateIssuer = _jwtOptions.ValidateIssuer,
ValidateAudience = _jwtOptions.ValidateAudience,
ValidateLifetime = _jwtOptions.ValidateLifetime
}));
// 创建对称安全密钥
if (string.IsNullOrEmpty(_jwtOptions.SecretKey))
{
_logger.LogError("JWT密钥未配置");
throw new InvalidOperationException("JWT密钥未配置");
}
// 验证密钥是否为有效的Base64字符串
try
{
var keyBytes = Convert.FromBase64String(_jwtOptions.SecretKey);
_logger.LogInformation("密钥Base64验证通过,字节长度: {Length}", keyBytes.Length);
}
catch (FormatException)
{
_logger.LogError("密钥不是有效的Base64字符串");
throw new InvalidOperationException("JWT密钥不是有效的Base64字符串");
}
// 验证受众
ValidateAudience = true,
ValidAudience = _jwtOptions.Audience,
var securityKey = new SymmetricSecurityKey(Convert.FromBase64String(_jwtOptions.SecretKey));
_logger.LogInformation("已创建安全密钥,密钥长度: {Length}", _jwtOptions.SecretKey.Length);
// 验证过期时间
ValidateLifetime = true,
// 配置令牌验证参数
options.TokenValidationParameters = new TokenValidationParameters
{
// 验证颁发者
ValidateIssuer = true,
ValidIssuer = _jwtOptions.Issuer,
// 设置签名密钥
IssuerSigningKey = securityKey,
ValidateIssuerSigningKey = true,
// 验证受众
ValidateAudience = true,
ValidAudience = _jwtOptions.Audience,
// 配置时钟偏差
ClockSkew = TimeSpan.FromMinutes(_jwtOptions.ClockSkewMinutes),
// 验证过期时间
ValidateLifetime = true,
// 其他安全设置
RequireExpirationTime = true,
RequireSignedTokens = true,
RequireAudience = true
};
// 设置签名密钥
IssuerSigningKey = securityKey,
ValidateIssuerSigningKey = true,
// 配置事件处理
options.Events = new JwtBearerEvents
{
// 认证失败时记录日志
OnAuthenticationFailed = context =>
{
_logger.LogError(context.Exception, "JWT 认证失败: {Message}", context.Exception?.Message);
return Task.CompletedTask;
},
// 配置时钟偏差
ClockSkew = TimeSpan.FromMinutes(_jwtOptions.ClockSkewMinutes),
// 其他安全设置
RequireExpirationTime = true,
RequireSignedTokens = true,
RequireAudience = true
};
// 令牌验证成功时
OnTokenValidated = context =>
_logger.LogInformation("JWT配置参数: {Params}", JsonSerializer.Serialize(new
{
var claimsIdentity = context.Principal?.Identity as System.Security.Claims.ClaimsIdentity;
if (claimsIdentity == null)
Issuer = _jwtOptions.Issuer,
Audience = _jwtOptions.Audience,
ValidateIssuer = options.TokenValidationParameters.ValidateIssuer,
ValidateAudience = options.TokenValidationParameters.ValidateAudience,
ValidateLifetime = options.TokenValidationParameters.ValidateLifetime,
ClockSkew = options.TokenValidationParameters.ClockSkew,
RequireExpirationTime = options.TokenValidationParameters.RequireExpirationTime,
RequireSignedTokens = options.TokenValidationParameters.RequireSignedTokens,
RequireAudience = options.TokenValidationParameters.RequireAudience,
HasSigningKey = options.TokenValidationParameters.IssuerSigningKey != null,
SigningKeyLength = options.TokenValidationParameters.IssuerSigningKey?.KeySize
}));
// 配置事件处理
options.Events = new JwtBearerEvents
{
// 认证失败时记录日志
OnAuthenticationFailed = context =>
{
context.Fail("无效的身份声明");
_logger.LogError(context.Exception, "JWT认证失败: {Message}", context.Exception?.Message);
_logger.LogError("认证失败详情: {Details}", JsonSerializer.Serialize(new
{
Path = context.Request.Path,
Scheme = context.Scheme.Name,
Exception = context.Exception?.ToString(),
Properties = context.Properties?.Items,
TokenValidationParameters = new
{
HasSigningKey = options.TokenValidationParameters.IssuerSigningKey != null,
ValidateIssuerSigningKey = options.TokenValidationParameters.ValidateIssuerSigningKey,
ValidIssuer = options.TokenValidationParameters.ValidIssuer,
ValidAudience = options.TokenValidationParameters.ValidAudience,
SigningKeyLength = options.TokenValidationParameters.IssuerSigningKey?.KeySize
}
}));
return Task.CompletedTask;
}
},
_logger.LogInformation("JWT 令牌验证成功: {Subject}",
claimsIdentity.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value);
return Task.CompletedTask;
},
// 令牌验证成功时
OnTokenValidated = context =>
{
var claimsIdentity = context.Principal?.Identity as System.Security.Claims.ClaimsIdentity;
if (claimsIdentity == null)
{
_logger.LogWarning("无效的身份声明");
context.Fail("无效的身份声明");
return Task.CompletedTask;
}
_logger.LogInformation("JWT令牌验证成功: {Subject}",
claimsIdentity.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value);
return Task.CompletedTask;
},
// 消息接收时
OnMessageReceived = context =>
{
// 从查询字符串中获取令牌
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken))
// 消息接收时
OnMessageReceived = context =>
{
context.Token = accessToken;
}
// 从查询字符串中获取令牌
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken))
{
_logger.LogInformation("从查询字符串获取到令牌");
context.Token = accessToken;
}
return Task.CompletedTask;
},
return Task.CompletedTask;
},
// 质询时
OnChallenge = context =>
{
_logger.LogWarning("JWT质询: {Error}, {Description}", context.Error, context.ErrorDescription);
return Task.CompletedTask;
}
};
// 质询时
OnChallenge = context =>
{
_logger.LogWarning("JWT 质询: {Error}", context.Error);
return Task.CompletedTask;
}
};
// 强制使用 HTTPS
options.RequireHttpsMetadata = true;
// 强制使用 HTTPS
options.RequireHttpsMetadata = true;
// 不保存令牌
options.SaveToken = false;
// 不保存令牌
options.SaveToken = false;
_logger.LogInformation("JWT Bearer选项配置完成");
}
catch (Exception ex)
{
_logger.LogError(ex, "配置JWT Bearer选项失败: {Message}", ex.Message);
throw;
}
}
}

148
src/CellularManagement.Infrastructure/Options/JwtOptions.cs

@ -46,7 +46,7 @@ public class JwtOptions
/// <summary>
/// 密钥最小长度
/// </summary>
public int MinKeyLength { get; set; } = 32;
public int MinKeyLength { get; set; } = 64;
/// <summary>
/// 颁发者
@ -81,7 +81,7 @@ public class JwtOptions
/// 2. 通常设置为 15-60 分钟
/// 3. 使用刷新令牌机制延长会话
/// </remarks>
public int ExpiryMinutes { get; set; } = 15; // 缩短默认过期时间
public int ExpiryMinutes { get; set; } = 15;
/// <summary>
/// 刷新令牌过期时间(天)
@ -95,92 +95,168 @@ public class JwtOptions
public int RefreshTokenExpiryDays { get; set; } = 7;
/// <summary>
/// 是否验证颁发者
/// 时钟偏差(分钟)
/// 允许的服务器时间偏差
/// </summary>
/// <remarks>
/// 建议:
/// 1. 生产环境应该启用
/// 2. 开发环境可以禁用
/// 3. 用于防止令牌伪造
/// 1. 设置合理的偏差值
/// 2. 通常设置为 5 分钟
/// 3. 用于处理服务器时间不同步的情况
/// </remarks>
public bool ValidateIssuer { get; set; } = true;
public int ClockSkewMinutes { get; set; } = 5;
/// <summary>
/// 是否验证受众
/// 是否要求 HTTPS
/// </summary>
/// <remarks>
/// 建议:
/// 1. 生产环境应该启用
/// 2. 开发环境可以禁用
/// 3. 用于防止令牌滥用
/// 3. 确保令牌传输安全
/// </remarks>
public bool ValidateAudience { get; set; } = true;
public bool RequireHttpsMetadata { get; set; } = true;
/// <summary>
/// 是否验证过期时间
/// 是否保存令牌
/// </summary>
/// <remarks>
/// 建议:
/// 1. 始终启
/// 2. 确保令牌不会永久有效
/// 3. 提高安全性
/// 1. 通常禁
/// 2. 用于调试目的
/// 3. 生产环境应该禁用
/// </remarks>
public bool ValidateLifetime { get; set; } = true;
public bool SaveToken { get; set; } = false;
/// <summary>
/// 时钟偏差(分钟)
/// 允许的服务器时间偏差
/// 是否验证颁发者
/// </summary>
/// <remarks>
/// 建议:
/// 1. 设置合理的偏差值
/// 2. 通常设置为 5 分钟
/// 3. 用于处理服务器时间不同步的情况
/// 1. 生产环境应该启用
/// 2. 开发环境可以禁用
/// 3. 用于防止令牌伪造
/// </remarks>
public int ClockSkewMinutes { get; set; } = 5;
public bool ValidateIssuer { get; set; } = true;
/// <summary>
/// 是否要求 HTTPS
/// 是否验证受众
/// </summary>
/// <remarks>
/// 建议:
/// 1. 生产环境应该启用
/// 2. 开发环境可以禁用
/// 3. 确保令牌传输安全
/// 3. 用于防止令牌滥用
/// </remarks>
public bool RequireHttpsMetadata { get; set; } = true;
public bool ValidateAudience { get; set; } = true;
/// <summary>
/// 是否保存令牌
/// 是否验证过期时间
/// </summary>
/// <remarks>
/// 建议:
/// 1. 通常禁
/// 2. 用于调试目的
/// 3. 生产环境应该禁用
/// 1. 始终启
/// 2. 确保令牌不会永久有效
/// 3. 提高安全性
/// </remarks>
public bool SaveToken { get; set; } = false;
public bool ValidateLifetime { get; set; } = true;
/// <summary>
/// 验证密钥强度
/// 验证配置
/// </summary>
public void ValidateKeyStrength()
public void Validate()
{
if (string.IsNullOrEmpty(SecretKey))
{
throw new ArgumentException("JWT 密钥不能为空");
throw new ArgumentException("JWT密钥不能为空");
}
if (SecretKey.Length < MinKeyLength)
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($"JWT 密钥长度必须至少为 {MinKeyLength} 字节");
throw new ArgumentException("密钥不是有效的Base64字符串");
}
// 检查密钥是否包含足够的随机性
var entropy = CalculateEntropy(SecretKey);
var entropy = CalculateEntropy(options.SecretKey);
if (entropy < 3.5) // 3.5 bits per character is considered good
{
throw new ArgumentException("JWT 密钥随机性不足");
throw new ArgumentException("密钥随机性不足");
}
}

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

@ -7,6 +7,7 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Caching.Memory;
using System.Text.Json;
namespace CellularManagement.Infrastructure.Services;
@ -73,6 +74,8 @@ public sealed class JwtProvider : IJwtProvider
{
try
{
_logger.LogInformation("开始验证令牌: {Token}", token);
// 检查令牌是否被撤销
if (IsTokenRevoked(token))
{
@ -87,12 +90,24 @@ public sealed class JwtProvider : IJwtProvider
return false;
}
// 获取当前密钥
var currentKey = GetCurrentKey();
_logger.LogInformation("验证令牌使用的密钥: {Key}", currentKey);
// 检查是否需要轮换密钥
if (_keyRotationService.ShouldRotateKey())
{
_logger.LogWarning("检测到密钥需要轮换");
}
// 解析令牌以获取算法信息
var jwtToken = _tokenHandler.ReadJwtToken(token);
_logger.LogInformation("令牌算法: {Algorithm}", jwtToken.SignatureAlgorithm);
// 使用Base64解码密钥
var keyBytes = Convert.FromBase64String(currentKey);
_logger.LogInformation("密钥解码成功,字节长度: {Length}", keyBytes.Length);
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
@ -100,7 +115,7 @@ public sealed class JwtProvider : IJwtProvider
ValidateAudience = true,
ValidAudience = _jwtOptions.Audience,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GetCurrentKey())),
IssuerSigningKey = new SymmetricSecurityKey(keyBytes),
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(_jwtOptions.ClockSkewMinutes),
RequireExpirationTime = true,
@ -108,9 +123,45 @@ public sealed class JwtProvider : IJwtProvider
RequireAudience = true
};
_logger.LogInformation("令牌验证参数: {Params}", JsonSerializer.Serialize(new
{
ValidateIssuer = tokenValidationParameters.ValidateIssuer,
ValidIssuer = tokenValidationParameters.ValidIssuer,
ValidateAudience = tokenValidationParameters.ValidateAudience,
ValidAudience = tokenValidationParameters.ValidAudience,
ValidateLifetime = tokenValidationParameters.ValidateLifetime,
ClockSkew = tokenValidationParameters.ClockSkew,
RequireExpirationTime = tokenValidationParameters.RequireExpirationTime,
RequireSignedTokens = tokenValidationParameters.RequireSignedTokens,
RequireAudience = tokenValidationParameters.RequireAudience,
Algorithm = jwtToken.SignatureAlgorithm,
KeyLength = keyBytes.Length
}));
_tokenHandler.ValidateToken(token, tokenValidationParameters, out _);
_logger.LogInformation("令牌验证成功");
return true;
}
catch (SecurityTokenExpiredException ex)
{
_logger.LogError(ex, "令牌已过期: {Message}", ex.Message);
return false;
}
catch (SecurityTokenInvalidSignatureException ex)
{
_logger.LogError(ex, "令牌签名无效: {Message}", ex.Message);
return false;
}
catch (SecurityTokenInvalidIssuerException ex)
{
_logger.LogError(ex, "令牌颁发者无效: {Message}", ex.Message);
return false;
}
catch (SecurityTokenInvalidAudienceException ex)
{
_logger.LogError(ex, "令牌受众无效: {Message}", ex.Message);
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, "令牌验证失败: {Message}", ex.Message);
@ -307,9 +358,18 @@ public sealed class JwtProvider : IJwtProvider
_logger.LogWarning("检测到密钥需要轮换");
}
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GetCurrentKey()));
var currentKey = GetCurrentKey();
_logger.LogInformation("生成令牌使用的密钥: {Key}", currentKey);
// 使用Base64解码密钥
var keyBytes = Convert.FromBase64String(currentKey);
_logger.LogInformation("密钥解码成功,字节长度: {Length}", keyBytes.Length);
var securityKey = new SymmetricSecurityKey(keyBytes);
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha512);
_logger.LogInformation("令牌签名算法: {Algorithm}", credentials.Algorithm);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
@ -325,7 +385,22 @@ public sealed class JwtProvider : IJwtProvider
tokenDescriptor.Subject.AddClaim(new Claim("token_type", tokenType));
var token = _tokenHandler.CreateToken(tokenDescriptor);
return _tokenHandler.WriteToken(token);
var tokenString = _tokenHandler.WriteToken(token);
// 记录令牌信息
_logger.LogInformation("生成的令牌信息: {TokenInfo}", JsonSerializer.Serialize(new
{
TokenType = tokenType,
Expires = tokenDescriptor.Expires,
Issuer = tokenDescriptor.Issuer,
Audience = tokenDescriptor.Audience,
IssuedAt = tokenDescriptor.IssuedAt,
NotBefore = tokenDescriptor.NotBefore,
Algorithm = credentials.Algorithm,
KeyLength = keyBytes.Length
}));
return tokenString;
}
catch (Exception ex)
{
@ -340,18 +415,53 @@ public sealed class JwtProvider : IJwtProvider
/// <returns>当前密钥</returns>
private string GetCurrentKey()
{
var cacheKey = $"{KeyCacheKey}Current";
if (_cacheService.TryGetValue<string>(cacheKey, out var cachedKey))
try
{
return cachedKey!;
}
var cacheKey = $"{KeyCacheKey}Current";
_logger.LogInformation("尝试从缓存获取密钥,缓存键: {CacheKey}", cacheKey);
var currentKey = _keyRotationService.GetCurrentKey();
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); // 缓存5分钟
if (_cacheService.TryGetValue<string>(cacheKey, out var cachedKey))
{
_logger.LogInformation("从缓存获取到密钥,密钥长度: {Length}", cachedKey?.Length ?? 0);
return cachedKey!;
}
_logger.LogInformation("缓存中未找到密钥,从KeyRotationService获取");
var currentKey = _keyRotationService.GetCurrentKey();
if (string.IsNullOrEmpty(currentKey))
{
_logger.LogError("从KeyRotationService获取的密钥为空");
throw new InvalidOperationException("JWT密钥不能为空");
}
_logger.LogInformation("从KeyRotationService获取到密钥,密钥长度: {Length}", currentKey.Length);
// 验证密钥格式
try
{
var keyBytes = Encoding.UTF8.GetBytes(currentKey);
_logger.LogInformation("密钥编码成功,字节长度: {Length}", keyBytes.Length);
}
catch (Exception ex)
{
_logger.LogError(ex, "密钥编码失败: {Message}", ex.Message);
throw new InvalidOperationException("JWT密钥格式无效", ex);
}
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); // 缓存5分钟
_cacheService.Set(cacheKey, currentKey, options);
_logger.LogInformation("密钥已缓存,过期时间: {Expiration}", DateTime.UtcNow.AddMinutes(5));
_cacheService.Set(cacheKey, currentKey, options);
return currentKey;
return currentKey;
}
catch (Exception ex)
{
_logger.LogError(ex, "获取当前密钥失败: {Message}", ex.Message);
throw;
}
}
/// <summary>

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

@ -58,7 +58,34 @@ public sealed class KeyRotationService : IKeyRotationService
/// <inheritdoc />
public string GetCurrentKey()
{
return _currentKey;
try
{
_logger.LogInformation("获取当前密钥");
if (string.IsNullOrEmpty(_currentKey))
{
_logger.LogError("当前密钥为空");
throw new InvalidOperationException("当前密钥未初始化");
}
// 验证密钥强度
ValidateKeyStrength(_currentKey);
_logger.LogInformation("当前密钥验证通过,密钥长度: {Length}", _currentKey.Length);
// 检查是否需要轮换
if (ShouldRotateKey())
{
_logger.LogWarning("检测到密钥需要轮换,当前密钥使用时间: {Duration}天",
(DateTime.UtcNow - _lastRotationTime).TotalDays);
}
return _currentKey;
}
catch (Exception ex)
{
_logger.LogError(ex, "获取当前密钥失败: {Message}", ex.Message);
throw;
}
}
/// <inheritdoc />
@ -104,10 +131,26 @@ public sealed class KeyRotationService : IKeyRotationService
/// </summary>
private string GenerateNewKey()
{
using var rng = RandomNumberGenerator.Create();
var keyBytes = new byte[_jwtOptions.MinKeyLength];
rng.GetBytes(keyBytes);
return Convert.ToBase64String(keyBytes);
try
{
using var rng = RandomNumberGenerator.Create();
var keyBytes = new byte[_jwtOptions.MinKeyLength];
rng.GetBytes(keyBytes);
// 确保生成的密钥是有效的Base64字符串
var key = Convert.ToBase64String(keyBytes);
_logger.LogInformation("生成新密钥成功,密钥长度: {Length}", key.Length);
// 验证生成的密钥
ValidateKeyStrength(key);
return key;
}
catch (Exception ex)
{
_logger.LogError(ex, "生成新密钥失败: {Message}", ex.Message);
throw;
}
}
/// <summary>
@ -115,21 +158,40 @@ public sealed class KeyRotationService : IKeyRotationService
/// </summary>
private void ValidateKeyStrength(string key)
{
if (string.IsNullOrEmpty(key))
try
{
throw new ArgumentException("密钥不能为空");
}
if (string.IsNullOrEmpty(key))
{
throw new ArgumentException("密钥不能为空");
}
if (key.Length < _jwtOptions.MinKeyLength)
{
throw new ArgumentException($"密钥长度必须至少为 {_jwtOptions.MinKeyLength} 字节");
}
// 验证密钥是否为有效的Base64字符串
try
{
var keyBytes = Convert.FromBase64String(key);
if (keyBytes.Length < _jwtOptions.MinKeyLength)
{
throw new ArgumentException($"密钥长度必须至少为 {_jwtOptions.MinKeyLength} 字节");
}
_logger.LogInformation("密钥Base64验证通过,字节长度: {Length}", keyBytes.Length);
}
catch (FormatException)
{
throw new ArgumentException("密钥不是有效的Base64字符串");
}
// 检查密钥是否包含足够的随机性
var entropy = CalculateEntropy(key);
if (entropy < 3.5) // 3.5 bits per character is considered good
// 检查密钥是否包含足够的随机性
var entropy = CalculateEntropy(key);
if (entropy < 3.5) // 3.5 bits per character is considered good
{
throw new ArgumentException("密钥随机性不足");
}
_logger.LogInformation("密钥熵值: {Entropy}", entropy);
}
catch (Exception ex)
{
throw new ArgumentException("密钥随机性不足");
_logger.LogError(ex, "密钥强度验证失败: {Message}", ex.Message);
throw;
}
}

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

@ -10,6 +10,8 @@ using CellularManagement.Infrastructure.Configurations;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Http;
using CellularManagement.Presentation.Abstractions;
using CellularManagement.Application.Features.Auth.Commands.Login;
using Swashbuckle.AspNetCore.Filters;
namespace CellularManagement.Presentation.Controllers;
@ -64,6 +66,7 @@ public class AuthController : ApiController
/// <response code="400">登录失败,返回错误信息</response>
/// <response code="429">登录尝试次数过多</response>
[HttpPost()]
[SwaggerRequestExample(typeof(LoginRequest), typeof(LoginRequestExample))]
[ProducesResponseType(typeof(OperationResult<AuthenticateUserResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<AuthenticateUserResponse>), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(OperationResult<AuthenticateUserResponse>), StatusCodes.Status429TooManyRequests)]
@ -72,12 +75,12 @@ public class AuthController : ApiController
try
{
// 检查登录尝试次数
var cacheKey = string.Format(_authConfig.LoginAttemptsCacheKeyFormat, command.UserNameOrEmail);
var cacheKey = string.Format(_authConfig.LoginAttemptsCacheKeyFormat, command.UserName);
var attempts = _cache.Get<int>(cacheKey);
if (attempts >= _authConfig.MaxLoginAttempts)
{
_logger.LogWarning("用户 {UserNameOrEmail} 登录尝试次数过多", command.UserNameOrEmail);
_logger.LogWarning("用户 {UserName} 登录尝试次数过多", command.UserName);
return StatusCode(StatusCodes.Status429TooManyRequests,
OperationResult<AuthenticateUserResponse>.CreateFailure("登录尝试次数过多,请稍后再试"));
}
@ -92,22 +95,22 @@ public class AuthController : ApiController
.SetAbsoluteExpiration(TimeSpan.FromMinutes(_authConfig.LoginAttemptsWindowMinutes));
_cache.Set(cacheKey, attempts + 1, options);
_logger.LogWarning("用户 {UserNameOrEmail} 登录失败: {Error}",
command.UserNameOrEmail,
_logger.LogWarning("用户 {UserName} 登录失败: {Error}",
command.UserName,
result.ErrorMessages?.FirstOrDefault());
}
else
{
// 清除登录尝试次数
_cache.Remove(cacheKey);
_logger.LogInformation("用户 {UserNameOrEmail} 登录成功", command.UserNameOrEmail);
_logger.LogInformation("用户 {UserName} 登录成功", command.UserName);
}
_logger.LogWarning($"Bearer {result.Data.AccessToken}");
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "用户 {UserNameOrEmail} 登录时发生异常", command.UserNameOrEmail);
_logger.LogError(ex, "用户 {UserName} 登录时发生异常", command.UserName);
return StatusCode(StatusCodes.Status500InternalServerError,
OperationResult<AuthenticateUserResponse>.CreateFailure("系统错误,请稍后重试"));
}

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

@ -18,6 +18,7 @@ namespace CellularManagement.Presentation.Controllers;
/// 用户管理控制器
/// 提供用户管理相关的 API 接口,包括用户信息的查询、更新和删除等功能
/// </summary>
[Authorize]
public class UsersController : ApiController
{
private readonly ILogger<UsersController> _logger;
@ -111,7 +112,6 @@ public class UsersController : ApiController
/// <response code="200">查询成功,返回用户列表</response>
/// <response code="400">查询失败,返回错误信息</response>
[HttpGet]
[AllowAnonymous]
[ProducesResponseType(typeof(OperationResult<GetAllUsersResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<object>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OperationResult<GetAllUsersResponse>>> GetUsers([FromQuery] GetAllUsersQuery query)
@ -299,4 +299,59 @@ public class UsersController : ApiController
OperationResult<object>.CreateFailure("系统错误,请稍后重试"));
}
}
/// <summary>
/// 获取当前登录用户信息
/// </summary>
/// <remarks>
/// 示例请求:
///
/// GET /api/users/current
///
/// </remarks>
/// <returns>
/// 查询结果,包含:
/// - 成功:返回当前用户详细信息
/// - 失败:返回错误信息
/// </returns>
/// <response code="200">查询成功,返回用户信息</response>
/// <response code="401">未授权,用户未登录</response>
/// <response code="404">用户不存在</response>
[HttpGet()]
[ProducesResponseType(typeof(OperationResult<GetUserByIdResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<object>), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(OperationResult<object>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<OperationResult<GetUserByIdResponse>>> CurrentUser()
{
try
{
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
_logger.LogWarning("获取当前用户信息失败:未找到用户ID");
return Unauthorized(OperationResult<object>.CreateFailure("未授权,请先登录"));
}
var query = new GetUserByIdQuery(userId);
var result = await mediator.Send(query);
if (result.IsSuccess)
{
_logger.LogInformation("获取当前用户信息成功,用户ID: {UserId}", userId);
return Ok(result);
}
else
{
_logger.LogWarning("获取当前用户信息失败: {Error}",
result.ErrorMessages?.FirstOrDefault());
return NotFound(result);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "获取当前用户信息时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError,
OperationResult<object>.CreateFailure("系统错误,请稍后重试"));
}
}
}

4
src/CellularManagement.WebAPI/CellularManagement.WebAPI.csproj

@ -6,6 +6,8 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -16,6 +18,8 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.8" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" />
</ItemGroup>
<ItemGroup>

88
src/CellularManagement.WebAPI/Program.cs

@ -12,10 +12,29 @@ using CellularManagement.WebSocket.Services;
using CellularManagement.WebSocket.Connection;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using CellularManagement.Infrastructure.Options;
using CellularManagement.Application.Features.Auth.Commands.Login;
using Swashbuckle.AspNetCore.Filters;
using Microsoft.IdentityModel.Tokens;
using System.Text;
// 创建 Web 应用程序构建器
var builder = WebApplication.CreateBuilder(args);
// 配置日志
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
builder.Logging.AddEventSourceLogger();
// 配置日志级别
builder.Logging.SetMinimumLevel(LogLevel.Information);
builder.Logging.AddFilter("Microsoft.AspNetCore.Authentication", LogLevel.Debug);
builder.Logging.AddFilter("Microsoft.AspNetCore.Authorization", LogLevel.Debug);
builder.Logging.AddFilter("System.IdentityModel.Tokens.Jwt", LogLevel.Debug);
// 注册基础设施层服务
// 包括数据库连接、缓存、日志等基础设施相关服务
builder.Services.AddInfrastructure(builder.Configuration);
@ -47,7 +66,48 @@ builder.Services.AddControllers()
// 启用 API 端点探索功能,用于生成 API 文档
builder.Services.AddEndpointsApiExplorer();
// 添加 Swagger 生成器服务,用于生成 OpenAPI 规范文档
builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
// 添加示例过滤器
options.ExampleFilters();
});
// 添加 Swagger 示例过滤器
builder.Services.AddSwaggerExamplesFromAssemblyOf<LoginRequestExample>();
// 配置 Swagger 文档
builder.Services.ConfigureSwaggerGen(options =>
{
options.EnableAnnotations();
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{typeof(Program).Assembly.GetName().Name}.xml"));
});
// 启用 Swagger 注释
builder.Services.AddSwaggerGenNewtonsoftSupport();
// 注册 MediatR 服务
// 从 WebAPI 程序集加载处理器,用于处理 Web 层特定的命令和查询
@ -64,6 +124,29 @@ builder.Services.AddCors();
// 从配置文件中读取认证相关配置
builder.Services.Configure<AuthConfiguration>(builder.Configuration.GetSection("Auth"));
// 配置 JWT Bearer 认证
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var jwtOptions = builder.Configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>();
if (jwtOptions == null)
{
throw new InvalidOperationException("JWT配置未找到");
}
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = jwtOptions.ValidateIssuer,
ValidIssuer = jwtOptions.Issuer,
ValidateAudience = jwtOptions.ValidateAudience,
ValidAudience = jwtOptions.Audience,
ValidateLifetime = jwtOptions.ValidateLifetime,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(jwtOptions.SecretKey)),
ClockSkew = TimeSpan.FromMinutes(jwtOptions.ClockSkewMinutes)
};
});
// 添加静态文件服务
builder.Services.AddDirectoryBrowser();
@ -88,6 +171,9 @@ app.UseHttpsRedirection();
// 允许所有来源、所有请求头、所有 HTTP 方法和凭证
app.UseCors(x => x.AllowAnyHeader().SetIsOriginAllowed(p => true).AllowAnyMethod().AllowCredentials());
// 启用认证中间件
app.UseAuthentication();
// 启用 WebSocket 中间件
app.UseWebSockets(new Microsoft.AspNetCore.Builder.WebSocketOptions
{

2
src/CellularManagement.WebAPI/appsettings.json

@ -12,7 +12,7 @@
"EnableSensitiveDataLogging": true
},
"JwtOptions": {
"SecretKey": "your-512-bit-secret-key-here-for-jwt-token-signing-this-is-a-very-long-key-that-is-at-least-512-bits-long-and-contains-enough-entropy",
"SecretKey": "a1mrtIiQN+AEmxE4WKFmKocGtrs3nrQaEbjzQgKp1XZWq8jP9HqzsjVgMKt3kAaCmTNaI9B9/YoaGMOY0sy8DQ==",
"Issuer": "CellularManagement",
"Audience": "CellularManagement.WebAPI",
"ExpiryMinutes": 15,

2
src/CellularManagement.WebSocket/Connection/WebSocketConnectionManager.cs

@ -227,7 +227,7 @@ public class WebSocketConnectionManager : IDisposable
public void Dispose()
{
_logger.LogInformation("正在释放 WebSocket 连接管理器资源");
_heartbeatTimer.Dispose();
_heartbeatTimer?.Dispose();
_logger.LogInformation("WebSocket 连接管理器资源已释放");
}
}

Loading…
Cancel
Save