using Avalonia_EFCore.Database; using Avalonia_EFCore.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using System.Security.Cryptography; using System.Text; namespace Avalonia_API.Authentication { /// /// Refresh Token 服务,负责创建、查找、撤销和轮换 Refresh Token, /// Token 原文经 SHA256 哈希后存入数据库以保证安全性。 /// public sealed class RefreshTokenService(AppDataContext db, IOptions options) { /// /// JWT 配置选项。 /// private readonly JwtOptions _options = options.Value; /// /// 创建一个新的 Refresh Token,生成随机 Token 原文并存储其哈希到数据库。 /// /// 关联的用户 ID。 /// 创建设备标识(如 User-Agent)。 /// 客户端 IP 地址。 /// 取消令牌。 /// 包含 Token 原文和实体记录的元组。 public async Task<(string Token, ApiRefreshTokenEntity Entity)> CreateAsync( int userId, string? device, string? ipAddress, CancellationToken cancellationToken = default) { var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); var entity = new ApiRefreshTokenEntity { UserId = userId, TokenHash = HashToken(token), ExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenDays), Device = device, IpAddress = ipAddress, }; db.ApiRefreshTokens.Add(entity); await db.SaveChangesAsync(cancellationToken); return (token, entity); } /// /// 查找有效的 Refresh Token 实体。Token 原文会被哈希后查询数据库, /// 仅返回未过期且未被撤销的 Token。 /// /// Refresh Token 原文。 /// 取消令牌。 /// 有效的 Token 实体;若无效或不存在则返回 null。 public async Task FindActiveAsync(string? token, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(token)) { return null; } var hash = HashToken(token); var entity = await db.ApiRefreshTokens.FirstOrDefaultAsync(x => x.TokenHash == hash, cancellationToken); return entity?.IsActive == true ? entity : null; } /// /// 撤销指定的 Refresh Token,将其 RevokedAt 设为当前时间。 /// /// 要撤销的 Refresh Token 原文。 /// 取消令牌。 public async Task RevokeAsync(string? token, CancellationToken cancellationToken = default) { var entity = await FindActiveAsync(token, cancellationToken); if (entity is null) { return; } entity.RevokedAt = DateTime.UtcNow; await db.SaveChangesAsync(cancellationToken); } /// /// 轮换 Refresh Token:撤销旧的并创建新的,将新 Token 的哈希关联到旧记录。 /// /// 旧的 Refresh Token 原文。 /// 当前设备标识。 /// 当前客户端 IP 地址。 /// 取消令牌。 /// 新的 Token 对;若旧 Token 无效则返回 null。 public async Task<(string Token, ApiRefreshTokenEntity Entity)?> RotateAsync( string? token, string? device, string? ipAddress, CancellationToken cancellationToken = default) { var current = await FindActiveAsync(token, cancellationToken); if (current is null) { return null; } var next = await CreateAsync(current.UserId, device, ipAddress, cancellationToken); current.RevokedAt = DateTime.UtcNow; current.ReplacedByTokenHash = next.Entity.TokenHash; await db.SaveChangesAsync(cancellationToken); return next; } /// /// 对 Token 原文进行 SHA256 哈希,返回十六进制字符串。 /// /// Token 原文。 /// SHA256 哈希后的十六进制字符串。 private static string HashToken(string token) { var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token)); return Convert.ToHexString(bytes); } } }