chore: 优化代码注释
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Options;
|
||||
@@ -9,22 +7,52 @@ namespace TakeoutSaaS.Infrastructure.Identity.Options;
|
||||
/// </summary>
|
||||
public sealed class AdminSeedOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始用户列表。
|
||||
/// </summary>
|
||||
public List<SeedUserOptions> Users { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 种子用户配置:用于初始化管理后台账号。
|
||||
/// </summary>
|
||||
public sealed class SeedUserOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录账号。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Account { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 登录密码(明文,将在初始化时进行哈希处理)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 展示名称。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 所属租户 ID。
|
||||
/// </summary>
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属商户 ID(平台管理员为空)。
|
||||
/// </summary>
|
||||
public Guid? MerchantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色集合。
|
||||
/// </summary>
|
||||
public string[] Roles { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 权限集合。
|
||||
/// </summary>
|
||||
public string[] Permissions { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
@@ -3,23 +3,38 @@ using System.ComponentModel.DataAnnotations;
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Options;
|
||||
|
||||
/// <summary>
|
||||
/// JWT 配置。
|
||||
/// JWT 配置选项。
|
||||
/// </summary>
|
||||
public sealed class JwtOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 令牌颁发者(Issuer)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Issuer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 令牌受众(Audience)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Audience { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// JWT 签名密钥(至少 32 个字符)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(32)]
|
||||
public string Secret { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 访问令牌过期时间(分钟),范围:5-1440。
|
||||
/// </summary>
|
||||
[Range(5, 1440)]
|
||||
public int AccessTokenExpirationMinutes { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新令牌过期时间(分钟),范围:60-20160(14天)。
|
||||
/// </summary>
|
||||
[Range(60, 1440 * 14)]
|
||||
public int RefreshTokenExpirationMinutes { get; set; } = 60 * 24 * 7;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,19 @@ using System.ComponentModel.DataAnnotations;
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Options;
|
||||
|
||||
/// <summary>
|
||||
/// 登录限流配置。
|
||||
/// 登录限流配置选项。
|
||||
/// </summary>
|
||||
public sealed class LoginRateLimitOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 时间窗口(秒),范围:1-3600。
|
||||
/// </summary>
|
||||
[Range(1, 3600)]
|
||||
public int WindowSeconds { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// 时间窗口内允许的最大尝试次数,范围:1-100。
|
||||
/// </summary>
|
||||
[Range(1, 100)]
|
||||
public int MaxAttempts { get; set; } = 5;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Options;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新令牌存储配置。
|
||||
/// 刷新令牌存储配置选项。
|
||||
/// </summary>
|
||||
public sealed class RefreshTokenStoreOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Redis 键前缀,用于存储刷新令牌。
|
||||
/// </summary>
|
||||
public string Prefix { get; set; } = "identity:refresh:";
|
||||
}
|
||||
|
||||
@@ -3,13 +3,19 @@ using System.ComponentModel.DataAnnotations;
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Options;
|
||||
|
||||
/// <summary>
|
||||
/// 微信小程序配置。
|
||||
/// 微信小程序配置选项。
|
||||
/// </summary>
|
||||
public sealed class WeChatMiniOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 微信小程序 AppId。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string AppId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 微信小程序 AppSecret。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Secret { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Infrastructure.Identity.Options;
|
||||
using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser;
|
||||
|
||||
@@ -17,29 +12,20 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
/// <summary>
|
||||
/// 后台账号初始化种子任务
|
||||
/// </summary>
|
||||
public sealed class IdentityDataSeeder : IHostedService
|
||||
public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger<IdentityDataSeeder> logger) : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<IdentityDataSeeder> _logger;
|
||||
|
||||
public IdentityDataSeeder(IServiceProvider serviceProvider, ILogger<IdentityDataSeeder> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<IdentityDbContext>();
|
||||
var options = scope.ServiceProvider.GetRequiredService<IOptions<AdminSeedOptions>>().Value;
|
||||
var passwordHasher = scope.ServiceProvider.GetRequiredService<IPasswordHasher<DomainIdentityUser>>();
|
||||
|
||||
await context.Database.MigrateAsync(cancellationToken);
|
||||
|
||||
if (options.Users == null || options.Users.Count == 0)
|
||||
if (options.Users is null or { Count: 0 })
|
||||
{
|
||||
_logger.LogInformation("AdminSeed 未配置账号,跳过后台账号初始化");
|
||||
logger.LogInformation("AdminSeed 未配置账号,跳过后台账号初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,7 +50,7 @@ public sealed class IdentityDataSeeder : IHostedService
|
||||
};
|
||||
user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password);
|
||||
context.IdentityUsers.Add(user);
|
||||
_logger.LogInformation("已创建后台账号 {Account}", user.Account);
|
||||
logger.LogInformation("已创建后台账号 {Account}", user.Account);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -74,7 +60,7 @@ public sealed class IdentityDataSeeder : IHostedService
|
||||
user.Roles = roles;
|
||||
user.Permissions = permissions;
|
||||
user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password);
|
||||
_logger.LogInformation("已更新后台账号 {Account}", user.Account);
|
||||
logger.LogInformation("已更新后台账号 {Account}", user.Account);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,10 +71,9 @@ public sealed class IdentityDataSeeder : IHostedService
|
||||
|
||||
private static string[] NormalizeValues(string[]? values)
|
||||
=> values == null
|
||||
? Array.Empty<string>()
|
||||
: values
|
||||
? []
|
||||
: [.. values
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v))
|
||||
.Select(v => v.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
@@ -11,12 +10,8 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
/// <summary>
|
||||
/// 身份认证 DbContext。
|
||||
/// </summary>
|
||||
public sealed class IdentityDbContext : DbContext
|
||||
public sealed class IdentityDbContext(DbContextOptions<IdentityDbContext> options) : DbContext(options)
|
||||
{
|
||||
public IdentityDbContext(DbContextOptions<IdentityDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<IdentityUser> IdentityUsers => Set<IdentityUser>();
|
||||
public DbSet<MiniUser> MiniUsers => Set<MiniUser>();
|
||||
@@ -37,11 +32,11 @@ public sealed class IdentityDbContext : DbContext
|
||||
builder.Property(x => x.Avatar).HasMaxLength(256);
|
||||
|
||||
var converter = new ValueConverter<string[], string>(
|
||||
v => string.Join(',', v ?? Array.Empty<string>()),
|
||||
v => string.Join(',', v),
|
||||
v => string.IsNullOrWhiteSpace(v) ? Array.Empty<string>() : v.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
||||
|
||||
var comparer = new ValueComparer<string[]>(
|
||||
(l, r) => l!.SequenceEqual(r!),
|
||||
(l, r) => (l == null && r == null) || (l != null && r != null && Enumerable.SequenceEqual(l, r)),
|
||||
v => v.Aggregate(0, (current, item) => HashCode.Combine(current, item.GetHashCode())),
|
||||
v => v.ToArray());
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||
@@ -16,29 +12,33 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services;
|
||||
/// <summary>
|
||||
/// JWT 令牌生成器。
|
||||
/// </summary>
|
||||
public sealed class JwtTokenService : IJwtTokenService
|
||||
public sealed class JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptions<JwtOptions> options) : IJwtTokenService
|
||||
{
|
||||
private readonly JwtSecurityTokenHandler _tokenHandler = new();
|
||||
private readonly IRefreshTokenStore _refreshTokenStore;
|
||||
private readonly JwtOptions _options;
|
||||
|
||||
public JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptions<JwtOptions> options)
|
||||
{
|
||||
_refreshTokenStore = refreshTokenStore;
|
||||
_options = options.Value;
|
||||
}
|
||||
private readonly JwtOptions _options = options.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 创建访问令牌和刷新令牌对。
|
||||
/// </summary>
|
||||
/// <param name="profile">用户档案</param>
|
||||
/// <param name="isNewUser">是否为新用户(首次登录)</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>令牌响应</returns>
|
||||
public async Task<TokenResponse> CreateTokensAsync(CurrentUserProfile profile, bool isNewUser = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var accessExpires = now.AddMinutes(_options.AccessTokenExpirationMinutes);
|
||||
var refreshExpires = now.AddMinutes(_options.RefreshTokenExpirationMinutes);
|
||||
|
||||
// 1. 构建 JWT Claims(包含用户 ID、账号、租户 ID、商户 ID、角色、权限等)
|
||||
var claims = BuildClaims(profile);
|
||||
|
||||
// 2. 创建签名凭据(使用 HMAC SHA256 算法)
|
||||
var signingCredentials = new SigningCredentials(
|
||||
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Secret)),
|
||||
SecurityAlgorithms.HmacSha256);
|
||||
|
||||
// 3. 创建 JWT 安全令牌
|
||||
var jwt = new JwtSecurityToken(
|
||||
issuer: _options.Issuer,
|
||||
audience: _options.Audience,
|
||||
@@ -47,8 +47,11 @@ public sealed class JwtTokenService : IJwtTokenService
|
||||
expires: accessExpires,
|
||||
signingCredentials: signingCredentials);
|
||||
|
||||
// 4. 序列化 JWT 为字符串
|
||||
var accessToken = _tokenHandler.WriteToken(jwt);
|
||||
var refreshDescriptor = await _refreshTokenStore.IssueAsync(profile.UserId, refreshExpires, cancellationToken);
|
||||
|
||||
// 5. 生成刷新令牌并存储到 Redis
|
||||
var refreshDescriptor = await refreshTokenStore.IssueAsync(profile.UserId, refreshExpires, cancellationToken);
|
||||
|
||||
return new TokenResponse
|
||||
{
|
||||
@@ -61,6 +64,11 @@ public sealed class JwtTokenService : IJwtTokenService
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 JWT Claims:将用户档案转换为 Claims 集合。
|
||||
/// </summary>
|
||||
/// <param name="profile">用户档案</param>
|
||||
/// <returns>Claims 集合</returns>
|
||||
private static IEnumerable<Claim> BuildClaims(CurrentUserProfile profile)
|
||||
{
|
||||
var userId = profile.UserId.ToString();
|
||||
|
||||
Reference in New Issue
Block a user