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;
}
}