- 从多个文件中移除未使用的 System.Linq.Dynamic.Core 引用 - 从多个文件中移除未使用的 Microsoft.EntityFrameworkCore 引用 - 从 IM 模块中移除未使用的 MongoDB.Driver.Linq 引用 - 清理 Handler 文件中的冗余命名空间引用 - 优化代码结构减少不必要的依赖引入 - 提高代码整洁性和编译效率
70 lines
3.0 KiB
C#
70 lines
3.0 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 刷新令牌Command处理器。执行业务校验后持久化。
|
||
/// </summary>
|
||
public class RefreshTokenCommandHandler(RagDbContext db, IConfiguration config) : IRequestHandler<RefreshTokenCommand, TokenResponse>
|
||
{
|
||
public async Task<TokenResponse> 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<Claim>
|
||
{
|
||
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));
|
||
}
|
||
}
|