FileShare/FileShare-API/Authentication/ApiAuthEndpointService.cs

139 lines
5.7 KiB
C#
Raw Permalink Normal View History

2026-05-22 14:29:22 +08:00
using FileShare_Common.Core;
using FileShare_EFCore.Database;
using FileShare_EFCore.Models;
using FileShare_Services.Core;
using FileShare_Services.Services.AuthService;
2026-05-21 15:52:36 +08:00
using Microsoft.EntityFrameworkCore;
2026-05-22 14:29:22 +08:00
namespace FileShare_API.Authentication
2026-05-21 15:52:36 +08:00
{
/// <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>
public async Task<IApiResponse> LoginAsync(ApiLoginRequest request, ServiceEndpointContext ctx)
2026-05-21 15:52:36 +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>
public async Task<IApiResponse> RefreshAsync(ApiRefreshTokenRequest request, ServiceEndpointContext ctx)
2026-05-21 15:52:36 +08:00
{
var rotated = await refreshTokenService.RotateAsync(
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>
public async Task<IApiResponse> LogoutAsync(ApiLogoutRequest request, ServiceEndpointContext ctx)
2026-05-21 15:52:36 +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"];
}
}
}