using BuildFlow.Shared.Domain.Exceptions; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using MediatR; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using BuildFlow.Application.Auth.DTOs; using BuildFlow.Infrastructure.Persistence; using BuildFlow.Application.Auth.Commands; namespace BuildFlow.Application.Auth.Handlers; /// /// 刷新令牌Command处理器。执行业务校验后持久化。 /// public class RefreshTokenCommandHandler(RagDbContext db, IConfiguration config) : IRequestHandler { public async Task Handle(RefreshTokenCommand request, CancellationToken ct) { var stored = await db.RefreshTokens .Include(rt => rt.User).ThenInclude(u => u.UserRoles).ThenInclude(ur => ur.Role).ThenInclude(r => r.RolePermissions).ThenInclude(rp => rp.Permission) .FirstOrDefaultAsync(rt => rt.Token == request.RefreshToken, ct) ?? throw new UnauthorizedException("无效的刷新令牌"); // 已吊销或已过期均视为不可用,防止 refresh token 被复用 if (stored.IsRevoked || stored.ExpiresAt < DateTime.UtcNow) throw new UnauthorizedException("刷新令牌已过期或已吊销"); // 旧 token 立即作废,强制下一次刷新必须用新 token(Rotation 防盗用) stored.IsRevoked = true; var user = stored.User; var roles = user.UserRoles.Select(ur => ur.Role.Name).ToList(); var permissions = user.UserRoles.SelectMany(ur => ur.Role.RolePermissions).Select(rp => rp.Permission.Code).Distinct().ToList(); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:SigningKey"]!)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new List { new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new(JwtRegisteredClaimNames.UniqueName, user.Username), new(JwtRegisteredClaimNames.Email, user.Email), }; claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r))); claims.AddRange(permissions.Select(p => new Claim("permissions", p))); var accessToken = new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken( issuer: config["Jwt:Issuer"], audience: config["Jwt:Audience"], claims: claims, expires: DateTime.UtcNow.AddMinutes(15), signingCredentials: creds )); var newRefresh = Convert.ToBase64String(Guid.NewGuid().ToByteArray()) + Guid.NewGuid().ToString("N"); await db.RefreshTokens.AddAsync(new Domain.Entities.RefreshToken { Id = Guid.NewGuid(), UserId = user.Id, Token = newRefresh, ExpiresAt = DateTime.UtcNow.AddDays(7), }, ct); await db.SaveChangesAsync(ct); return new TokenResponse(accessToken, newRefresh, DateTime.UtcNow.AddMinutes(15)); } }