- 新增 API 端 JWT 登录、refresh token 轮换和退出登录流程 - 新增 refresh token 实体、DbSet 配置和 EF Core 迁移 - 新增 PC 端授权码登录、本地全局 token 刷新、登出和鉴权服务 - 扩展统一端点模型,支持宿主过滤、角色鉴权、OpenAPI 元数据和 DI 服务处理器 - API 启用 JwtBearer 认证、Swagger UI 和认证端点注册 - PC 端注册认证服务,并按宿主过滤桌面拦截端点
124 lines
4.3 KiB
C#
124 lines
4.3 KiB
C#
using Avalonia_Common.Core;
|
|
using Avalonia_EFCore.Database;
|
|
using Avalonia_EFCore.Models;
|
|
using Avalonia_Services.Core;
|
|
using Avalonia_Services.Services.AuthService;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Text.Json;
|
|
|
|
namespace Avalonia_API.Authentication
|
|
{
|
|
public sealed class ApiAuthEndpointService(
|
|
AppDataContext db,
|
|
JwtTokenService jwtTokenService,
|
|
RefreshTokenService refreshTokenService) : IApiAuthEndpointService
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
};
|
|
|
|
public async Task<object?> LoginAsync(ServiceEndpointContext ctx)
|
|
{
|
|
var request = Deserialize<ApiLoginRequest>(ctx.Body);
|
|
if (string.IsNullOrWhiteSpace(request?.Account))
|
|
{
|
|
ctx.StatusCode = 400;
|
|
return ResponseHelper.Failure(400, "账号不能为空");
|
|
}
|
|
|
|
var user = await db.Users.FirstOrDefaultAsync(
|
|
x => x.Email == request.Account || x.Name == request.Account);
|
|
|
|
if (user is null)
|
|
{
|
|
user = new UserEntity
|
|
{
|
|
Name = request.Account,
|
|
Email = request.Account.Contains('@') ? request.Account : null,
|
|
};
|
|
db.Users.Add(user);
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
var roles = NormalizeRoles(request.Roles);
|
|
var accessToken = jwtTokenService.CreateAccessToken(user, roles);
|
|
var refreshToken = await refreshTokenService.CreateAsync(
|
|
user.Id,
|
|
ctx.GetHeader("User-Agent"),
|
|
GetRemoteIpAddress(ctx));
|
|
|
|
return ResponseHelper.Ok(new AuthTokenResponse(
|
|
accessToken.Token,
|
|
refreshToken.Token,
|
|
accessToken.ExpiresAt,
|
|
refreshToken.Entity.ExpiresAt,
|
|
roles), "登录成功");
|
|
}
|
|
|
|
public async Task<object?> RefreshAsync(ServiceEndpointContext ctx)
|
|
{
|
|
var request = Deserialize<ApiRefreshTokenRequest>(ctx.Body);
|
|
var rotated = await refreshTokenService.RotateAsync(
|
|
request?.RefreshToken,
|
|
ctx.GetHeader("User-Agent"),
|
|
GetRemoteIpAddress(ctx));
|
|
|
|
if (rotated is null)
|
|
{
|
|
ctx.StatusCode = 401;
|
|
return ResponseHelper.Failure(401, "刷新 token 无效或已过期");
|
|
}
|
|
|
|
var user = await db.Users.FindAsync(rotated.Value.Entity.UserId);
|
|
if (user is null)
|
|
{
|
|
ctx.StatusCode = 401;
|
|
return ResponseHelper.Failure(401, "用户不存在");
|
|
}
|
|
|
|
var roles = new[] { "Admin" };
|
|
var accessToken = jwtTokenService.CreateAccessToken(user, roles);
|
|
|
|
return ResponseHelper.Ok(new AuthTokenResponse(
|
|
accessToken.Token,
|
|
rotated.Value.Token,
|
|
accessToken.ExpiresAt,
|
|
rotated.Value.Entity.ExpiresAt,
|
|
roles), "刷新成功");
|
|
}
|
|
|
|
public async Task<object?> LogoutAsync(ServiceEndpointContext ctx)
|
|
{
|
|
var request = Deserialize<ApiLogoutRequest>(ctx.Body);
|
|
await refreshTokenService.RevokeAsync(request?.RefreshToken);
|
|
return ResponseHelper.Succeed("退出成功");
|
|
}
|
|
|
|
private static T? Deserialize<T>(string? body)
|
|
{
|
|
return string.IsNullOrWhiteSpace(body)
|
|
? default
|
|
: JsonSerializer.Deserialize<T>(body, JsonOptions);
|
|
}
|
|
|
|
private static string? GetRemoteIpAddress(ServiceEndpointContext ctx)
|
|
{
|
|
return ctx.Items.TryGetValue("HttpContext", out var value) && value is HttpContext httpContext
|
|
? httpContext.Connection.RemoteIpAddress?.ToString()
|
|
: null;
|
|
}
|
|
|
|
private static string[] NormalizeRoles(string[]? roles)
|
|
{
|
|
var normalized = roles?
|
|
.Where(role => !string.IsNullOrWhiteSpace(role))
|
|
.Select(role => role.Trim())
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
|
|
return normalized is { Length: > 0 } ? normalized : ["Admin"];
|
|
}
|
|
}
|
|
}
|