using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Infrastructure.Identity.Options; namespace TakeoutSaaS.Infrastructure.Identity.Services; /// /// JWT 令牌生成器。 /// public sealed class JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptions options) : IJwtTokenService { private readonly JwtSecurityTokenHandler _tokenHandler = new(); private readonly JwtOptions _options = options.Value; /// /// 创建访问令牌和刷新令牌对。 /// /// 用户档案 /// 是否为新用户(首次登录) /// 取消令牌 /// 令牌响应 public async Task 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, claims: claims, notBefore: now, expires: accessExpires, signingCredentials: signingCredentials); // 4. 序列化 JWT 为字符串 var accessToken = _tokenHandler.WriteToken(jwt); // 5. 生成刷新令牌并存储到 Redis var refreshDescriptor = await refreshTokenStore.IssueAsync(profile.UserId, refreshExpires, cancellationToken); return new TokenResponse { AccessToken = accessToken, AccessTokenExpiresAt = accessExpires, RefreshToken = refreshDescriptor.Token, RefreshTokenExpiresAt = refreshDescriptor.ExpiresAt, User = profile, IsNewUser = isNewUser }; } /// /// 构建 JWT Claims:将用户档案转换为 Claims 集合。 /// /// 用户档案 /// Claims 集合 private static List BuildClaims(CurrentUserProfile profile) { var userId = profile.UserId.ToString(); var claims = new List { new(JwtRegisteredClaimNames.Sub, userId), new(ClaimTypes.NameIdentifier, userId), new(JwtRegisteredClaimNames.UniqueName, profile.Account), new("tenant_id", profile.TenantId.ToString()), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; if (profile.MerchantId.HasValue) { claims.Add(new Claim("merchant_id", profile.MerchantId.Value.ToString())); } claims.AddRange(profile.Roles.Select(role => new Claim(ClaimTypes.Role, role))); claims.AddRange(profile.Permissions.Select(permission => new Claim("permission", permission))); return claims; } }