125 lines
5.1 KiB
C#
125 lines
5.1 KiB
C#
|
|
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
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// Refresh Token 服务,负责创建、查找、撤销和轮换 Refresh Token,
|
|||
|
|
/// Token 原文经 SHA256 哈希后存入数据库以保证安全性。
|
|||
|
|
/// </summary>
|
|||
|
|
public sealed class RefreshTokenService(AppDataContext db, IOptions<JwtOptions> options)
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// JWT 配置选项。
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly JwtOptions _options = options.Value;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 创建一个新的 Refresh Token,生成随机 Token 原文并存储其哈希到数据库。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="userId">关联的用户 ID。</param>
|
|||
|
|
/// <param name="device">创建设备标识(如 User-Agent)。</param>
|
|||
|
|
/// <param name="ipAddress">客户端 IP 地址。</param>
|
|||
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|||
|
|
/// <returns>包含 Token 原文和实体记录的元组。</returns>
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 查找有效的 Refresh Token 实体。Token 原文会被哈希后查询数据库,
|
|||
|
|
/// 仅返回未过期且未被撤销的 Token。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="token">Refresh Token 原文。</param>
|
|||
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|||
|
|
/// <returns>有效的 Token 实体;若无效或不存在则返回 null。</returns>
|
|||
|
|
public async Task<ApiRefreshTokenEntity?> 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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 撤销指定的 Refresh Token,将其 RevokedAt 设为当前时间。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="token">要撤销的 Refresh Token 原文。</param>
|
|||
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 轮换 Refresh Token:撤销旧的并创建新的,将新 Token 的哈希关联到旧记录。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="token">旧的 Refresh Token 原文。</param>
|
|||
|
|
/// <param name="device">当前设备标识。</param>
|
|||
|
|
/// <param name="ipAddress">当前客户端 IP 地址。</param>
|
|||
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|||
|
|
/// <returns>新的 Token 对;若旧 Token 无效则返回 null。</returns>
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 对 Token 原文进行 SHA256 哈希,返回十六进制字符串。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="token">Token 原文。</param>
|
|||
|
|
/// <returns>SHA256 哈希后的十六进制字符串。</returns>
|
|||
|
|
private static string HashToken(string token)
|
|||
|
|
{
|
|||
|
|
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
|
|||
|
|
return Convert.ToHexString(bytes);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|