feat(auth): 添加统一 API 和 PC 认证端点
- 新增 API 端 JWT 登录、refresh token 轮换和退出登录流程 - 新增 refresh token 实体、DbSet 配置和 EF Core 迁移 - 新增 PC 端授权码登录、本地全局 token 刷新、登出和鉴权服务 - 扩展统一端点模型,支持宿主过滤、角色鉴权、OpenAPI 元数据和 DI 服务处理器 - API 启用 JwtBearer 认证、Swagger UI 和认证端点注册 - PC 端注册认证服务,并按宿主过滤桌面拦截端点
This commit is contained in:
@@ -12,15 +12,19 @@ namespace Avalonia_Services.Core
|
||||
/// <summary>
|
||||
/// 打印所有已注册端点到控制台。
|
||||
/// </summary>
|
||||
public static void PrintEndpoints(ServiceEndpointCollection collection, string? title = null)
|
||||
public static void PrintEndpoints(
|
||||
ServiceEndpointCollection collection,
|
||||
string? title = null,
|
||||
EndpointHostTarget host = EndpointHostTarget.All)
|
||||
{
|
||||
title ??= "API Endpoints";
|
||||
var endpoints = collection.ForHost(host).ToList();
|
||||
|
||||
var maxMethodLen = collection.Endpoints.Count > 0
|
||||
? collection.Endpoints.Max(e => e.HttpMethod.Length)
|
||||
var maxMethodLen = endpoints.Count > 0
|
||||
? endpoints.Max(e => e.HttpMethod.Length)
|
||||
: 4;
|
||||
var maxPathLen = collection.Endpoints.Count > 0
|
||||
? collection.Endpoints.Max(e => e.Pattern.Length)
|
||||
var maxPathLen = endpoints.Count > 0
|
||||
? endpoints.Max(e => e.Pattern.Length)
|
||||
: 8;
|
||||
|
||||
var totalWidth = maxMethodLen + maxPathLen + 5;
|
||||
@@ -31,9 +35,11 @@ namespace Avalonia_Services.Core
|
||||
Console.WriteLine($"║ {"Method".PadRight(maxMethodLen)} │ {"Path".PadRight(maxPathLen)} │ Auth ║");
|
||||
Console.WriteLine($"╟{separator}╢");
|
||||
|
||||
foreach (var ep in collection.Endpoints.OrderBy(e => e.Pattern))
|
||||
foreach (var ep in endpoints.OrderBy(e => e.Pattern))
|
||||
{
|
||||
var auth = ep.RequireAuthorization ? (ep.Policy ?? "✓") : "—";
|
||||
var auth = ep.RequireAuthorization
|
||||
? (ep.Roles.Count > 0 ? string.Join(",", ep.Roles) : ep.Policy ?? "✓")
|
||||
: "—";
|
||||
var methodColor = ep.HttpMethod switch
|
||||
{
|
||||
"GET" => ConsoleColor.Green,
|
||||
@@ -57,7 +63,7 @@ namespace Avalonia_Services.Core
|
||||
}
|
||||
|
||||
Console.WriteLine($"╚{separator}╝");
|
||||
Console.WriteLine($" Total: {collection.Endpoints.Count} endpoint(s)");
|
||||
Console.WriteLine($" Total: {endpoints.Count} endpoint(s)");
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Avalonia_Services.Core
|
||||
{
|
||||
[Flags]
|
||||
public enum EndpointHostTarget
|
||||
{
|
||||
Api = 1,
|
||||
Pc = 2,
|
||||
All = Api | Pc,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个端点定义。
|
||||
/// </summary>
|
||||
@@ -18,6 +27,21 @@ namespace Avalonia_Services.Core
|
||||
/// <summary>端点名称(用于 OpenAPI / 日志)</summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>OpenAPI 分组标签。</summary>
|
||||
public string? OpenApiTag { get; set; }
|
||||
|
||||
/// <summary>OpenAPI 摘要。</summary>
|
||||
public string? OpenApiSummary { get; set; }
|
||||
|
||||
/// <summary>OpenAPI 描述。</summary>
|
||||
public string? OpenApiDescription { get; set; }
|
||||
|
||||
/// <summary>OpenAPI 请求体类型。</summary>
|
||||
public Type? OpenApiRequestType { get; set; }
|
||||
|
||||
/// <summary>OpenAPI 200 响应数据类型。</summary>
|
||||
public Type? OpenApiResponseType { get; set; }
|
||||
|
||||
/// <summary>端点处理器</summary>
|
||||
public Func<ServiceEndpointContext, Task<object?>> Handler { get; init; } = _ => Task.FromResult<object?>(null);
|
||||
|
||||
@@ -30,6 +54,12 @@ namespace Avalonia_Services.Core
|
||||
/// <summary>鉴权策略名</summary>
|
||||
public string? Policy { get; set; }
|
||||
|
||||
/// <summary>允许访问该端点的角色。多个角色满足任意一个即可。</summary>
|
||||
public List<string> Roles { get; } = new();
|
||||
|
||||
/// <summary>端点挂载的宿主。默认 API 和 PC 都挂载。</summary>
|
||||
public EndpointHostTarget HostTarget { get; set; } = EndpointHostTarget.All;
|
||||
|
||||
/// <summary>
|
||||
/// 设置端点名称(Fluent API)。
|
||||
/// </summary>
|
||||
@@ -38,6 +68,64 @@ namespace Avalonia_Services.Core
|
||||
Name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ServiceEndpoint WithOpenApi(
|
||||
string tag,
|
||||
string summary,
|
||||
string? description = null,
|
||||
Type? requestType = null,
|
||||
Type? responseType = null)
|
||||
{
|
||||
OpenApiTag = tag;
|
||||
OpenApiSummary = summary;
|
||||
OpenApiDescription = description;
|
||||
OpenApiRequestType = requestType;
|
||||
OpenApiResponseType = responseType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记端点需要登录。
|
||||
/// </summary>
|
||||
public ServiceEndpoint RequireAuth()
|
||||
{
|
||||
RequireAuthorization = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记端点需要指定角色。多个角色满足任意一个即可。
|
||||
/// </summary>
|
||||
public ServiceEndpoint RequireRoles(params string[] roles)
|
||||
{
|
||||
RequireAuthorization = true;
|
||||
Roles.Clear();
|
||||
Roles.AddRange(roles.Where(role => !string.IsNullOrWhiteSpace(role)).Select(role => role.Trim()));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 只挂载到 Avalonia-API。
|
||||
/// </summary>
|
||||
public ServiceEndpoint ApiOnly()
|
||||
{
|
||||
HostTarget = EndpointHostTarget.Api;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 只挂载到 Avalonia-PC。
|
||||
/// </summary>
|
||||
public ServiceEndpoint PcOnly()
|
||||
{
|
||||
HostTarget = EndpointHostTarget.Pc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool SupportsHost(EndpointHostTarget host)
|
||||
{
|
||||
return (HostTarget & host) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -48,6 +136,11 @@ namespace Avalonia_Services.Core
|
||||
/// <summary>所有已注册的端点</summary>
|
||||
public List<ServiceEndpoint> Endpoints { get; } = new();
|
||||
|
||||
public IEnumerable<ServiceEndpoint> ForHost(EndpointHostTarget host)
|
||||
{
|
||||
return Endpoints.Where(endpoint => endpoint.SupportsHost(host));
|
||||
}
|
||||
|
||||
/// <summary>作用于所有端点的全局过滤器</summary>
|
||||
public List<IEndpointFilter> GlobalFilters { get; } = new();
|
||||
|
||||
@@ -59,6 +152,14 @@ namespace Avalonia_Services.Core
|
||||
return AddEndpoint(pattern, "GET", handler);
|
||||
}
|
||||
|
||||
public ServiceEndpoint MapGet<TService>(
|
||||
string pattern,
|
||||
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
||||
where TService : notnull
|
||||
{
|
||||
return MapGet(pattern, CreateServiceHandler(handler));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个 POST 端点。
|
||||
/// </summary>
|
||||
@@ -67,6 +168,14 @@ namespace Avalonia_Services.Core
|
||||
return AddEndpoint(pattern, "POST", handler);
|
||||
}
|
||||
|
||||
public ServiceEndpoint MapPost<TService>(
|
||||
string pattern,
|
||||
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
||||
where TService : notnull
|
||||
{
|
||||
return MapPost(pattern, CreateServiceHandler(handler));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个 PUT 端点。
|
||||
/// </summary>
|
||||
@@ -75,6 +184,14 @@ namespace Avalonia_Services.Core
|
||||
return AddEndpoint(pattern, "PUT", handler);
|
||||
}
|
||||
|
||||
public ServiceEndpoint MapPut<TService>(
|
||||
string pattern,
|
||||
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
||||
where TService : notnull
|
||||
{
|
||||
return MapPut(pattern, CreateServiceHandler(handler));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个 DELETE 端点。
|
||||
/// </summary>
|
||||
@@ -83,6 +200,14 @@ namespace Avalonia_Services.Core
|
||||
return AddEndpoint(pattern, "DELETE", handler);
|
||||
}
|
||||
|
||||
public ServiceEndpoint MapDelete<TService>(
|
||||
string pattern,
|
||||
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
||||
where TService : notnull
|
||||
{
|
||||
return MapDelete(pattern, CreateServiceHandler(handler));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加全局过滤器(作用于所有端点)。
|
||||
/// </summary>
|
||||
@@ -112,6 +237,21 @@ namespace Avalonia_Services.Core
|
||||
Endpoints.Add(endpoint);
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
private static Func<ServiceEndpointContext, Task<object?>> CreateServiceHandler<TService>(
|
||||
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
||||
where TService : notnull
|
||||
{
|
||||
return async ctx =>
|
||||
{
|
||||
var serviceProvider = ctx.Items["ServiceProvider"] as IServiceProvider
|
||||
?? throw new InvalidOperationException("ServiceProvider 未注入。");
|
||||
|
||||
await using var scope = serviceProvider.CreateAsyncScope();
|
||||
var service = scope.ServiceProvider.GetRequiredService<TService>();
|
||||
return await handler(service, ctx);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user