AvaloniaStack/Avalonia-API/Authentication/RefreshTokenService.cs
luoqian a9abd90874 feat(auth): 添加统一 API 和 PC 认证端点
- 新增 API 端 JWT 登录、refresh token 轮换和退出登录流程
- 新增 refresh token 实体、DbSet 配置和 EF Core 迁移
- 新增 PC 端授权码登录、本地全局 token 刷新、登出和鉴权服务
- 扩展统一端点模型,支持宿主过滤、角色鉴权、OpenAPI 元数据和 DI 服务处理器
- API 启用 JwtBearer 认证、Swagger UI 和认证端点注册
- PC 端注册认证服务,并按宿主过滤桌面拦截端点
2026-05-15 17:35:07 +08:00

85 lines
2.9 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
{
public sealed class RefreshTokenService(AppDataContext db, IOptions<JwtOptions> options)
{
private readonly JwtOptions _options = options.Value;
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);
}
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;
}
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);
}
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;
}
private static string HashToken(string token)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
return Convert.ToHexString(bytes);
}
}
}