2026-05-21 15:52:36 +08:00
|
|
|
|
using Avalonia_Common.Core;
|
|
|
|
|
|
using Avalonia_EFCore.Database;
|
|
|
|
|
|
using Avalonia_EFCore.Models;
|
|
|
|
|
|
using Avalonia_Services.Core;
|
|
|
|
|
|
using Avalonia_Services.Services.AuthService;
|
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Avalonia_API.Authentication
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// API 鉴权端点服务,实现 <see cref="IApiAuthEndpointService"/>,
|
|
|
|
|
|
/// 处理登录、刷新 Token 和登出操作,使用 JWT 与 Refresh Token 机制。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public sealed class ApiAuthEndpointService(
|
|
|
|
|
|
AppDataContext db,
|
|
|
|
|
|
JwtTokenService jwtTokenService,
|
|
|
|
|
|
RefreshTokenService refreshTokenService) : IApiAuthEndpointService
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 处理用户登录请求。根据账号(邮箱或用户名)查找或创建用户,
|
|
|
|
|
|
/// 生成 JWT Access Token 和 Refresh Token 并返回。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="ctx">服务端点上下文,包含请求体、请求头等信息。</param>
|
|
|
|
|
|
/// <returns>包含 AccessToken、RefreshToken 及过期时间的认证响应。</returns>
|
2026-05-22 11:18:47 +08:00
|
|
|
|
public async Task<IApiResponse> LoginAsync(ApiLoginRequest request, ServiceEndpointContext ctx)
|
2026-05-21 15:52:36 +08:00
|
|
|
|
{
|
2026-05-22 11:18:47 +08:00
|
|
|
|
if (string.IsNullOrWhiteSpace(request.Account))
|
2026-05-21 15:52:36 +08:00
|
|
|
|
{
|
|
|
|
|
|
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), "登录成功");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 使用 Refresh Token 轮换新的 Access Token 和 Refresh Token。
|
|
|
|
|
|
/// 旧的 Refresh Token 会被撤销并替换。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="ctx">服务端点上下文,包含请求体中的 RefreshToken。</param>
|
|
|
|
|
|
/// <returns>新的 Token 对;若 Refresh Token 无效则返回 401 错误。</returns>
|
2026-05-22 11:18:47 +08:00
|
|
|
|
public async Task<IApiResponse> RefreshAsync(ApiRefreshTokenRequest request, ServiceEndpointContext ctx)
|
2026-05-21 15:52:36 +08:00
|
|
|
|
{
|
|
|
|
|
|
var rotated = await refreshTokenService.RotateAsync(
|
2026-05-22 11:18:47 +08:00
|
|
|
|
request.RefreshToken,
|
2026-05-21 15:52:36 +08:00
|
|
|
|
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), "刷新成功");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 处理用户登出请求,撤销指定的 Refresh Token。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="ctx">服务端点上下文,包含请求体中的 RefreshToken。</param>
|
|
|
|
|
|
/// <returns>登出成功的响应。</returns>
|
2026-05-22 11:18:47 +08:00
|
|
|
|
public async Task<IApiResponse> LogoutAsync(ApiLogoutRequest request, ServiceEndpointContext ctx)
|
2026-05-21 15:52:36 +08:00
|
|
|
|
{
|
2026-05-22 11:18:47 +08:00
|
|
|
|
await refreshTokenService.RevokeAsync(request.RefreshToken);
|
2026-05-21 15:52:36 +08:00
|
|
|
|
return ResponseHelper.Succeed("退出成功");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 从上下文的 Items 中提取 ASP.NET Core HttpContext,并获取客户端远程 IP 地址。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="ctx">服务端点上下文。</param>
|
|
|
|
|
|
/// <returns>客户端 IP 地址字符串;若无法获取则返回 null。</returns>
|
|
|
|
|
|
private static string? GetRemoteIpAddress(ServiceEndpointContext ctx)
|
|
|
|
|
|
{
|
|
|
|
|
|
return ctx.Items.TryGetValue("HttpContext", out var value) && value is HttpContext httpContext
|
|
|
|
|
|
? httpContext.Connection.RemoteIpAddress?.ToString()
|
|
|
|
|
|
: null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 规范化角色数组:去空白、去重(忽略大小写),为空时默认返回 Admin 角色。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="roles">原始角色数组,可为 null。</param>
|
|
|
|
|
|
/// <returns>规范化后的角色数组。</returns>
|
|
|
|
|
|
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"];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|