using FileShare_EFCore.Database;
using FileShare_EFCore.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System.Security.Cryptography;
using System.Text;
namespace FileShare_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);
}
}
}