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