using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using MoShiro.Backend.Application; using MoShiro.Protocol; namespace MoShiro.Backend.Api; public static class HttpContextPrincipalExtensions { private const string Key = "moshiro.principal"; public static Principal GetPrincipal(this HttpContext http) => http.Items.TryGetValue(Key, out object? value) && value is Principal principal ? principal : Principal.Anonymous; public static void SetPrincipal(this HttpContext http, Principal principal) => http.Items[Key] = principal; } /// /// 认证:Authorization: Bearer <api_token> 或会话 Cookie。作为中间件运行,只解析身份;授权由各层的守卫决定。 /// public static class PrincipalResolver { public const string SessionCookie = "moshiro_session"; public const string CsrfHeader = "X-CSRF-Token"; public static string? BearerOf(HttpContext http) { string? header = http.Request.Headers.Authorization; return header is not null && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) ? header["Bearer ".Length..].Trim() : null; } public static async Task ResolveAsync(HttpContext http, Func next) { AuthService auth = http.RequestServices.GetRequiredService(); Principal? principal = null; string? bearer = BearerOf(http); if (bearer is not null) { principal = await auth.AuthenticateTokenAsync(bearer, http.RequestAborted); if (principal is null && await auth.AuthenticateSessionAsync(bearer, http.RequestAborted) is { } viaSession) { // 登录时用 return_session 拿到的会话令牌:和 API 令牌一样走 Bearer,不是 Cookie,所以不需要 CSRF principal = viaSession with { Kind = PrincipalKind.Token, CsrfToken = null }; } } else if (http.Request.Cookies.TryGetValue(SessionCookie, out string? session) && !string.IsNullOrEmpty(session)) { principal = await auth.AuthenticateSessionAsync(session, http.RequestAborted); } http.SetPrincipal(principal ?? Principal.Anonymous); await next(); } } /// 管理层守卫:需要 admin 作用域;Cookie 会话的非 GET 请求必须带 X-CSRF-Token。 public static class AdminGuard { public static async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { HttpContext http = context.HttpContext; Principal principal = http.GetPrincipal(); if (!principal.IsAdmin) { return principal.Scope != ApiScope.None ? Problems.Create(http, MoShiroErrors.Forbidden, "the token scope does not allow this operation") : Problems.Create(http, MoShiroErrors.Unauthorized, "authentication is required"); } if (principal.Kind == PrincipalKind.Session && !HttpMethods.IsGet(http.Request.Method) && !HttpMethods.IsHead(http.Request.Method) && !HttpMethods.IsOptions(http.Request.Method)) { string? csrf = http.Request.Headers[PrincipalResolver.CsrfHeader]; if (csrf is null || principal.CsrfToken is null || !AuthService.FixedTimeEquals(csrf, principal.CsrfToken)) { return Problems.Create(http, MoShiroErrors.CsrfFailed, "a valid X-CSRF-Token header is required"); } } return await next(context); } } /// 登记发布清单的守卫:管理员(会话要带 CSRF)或 release 令牌;release 令牌只能做这一件事。 public static class ReleaseGuard { public static async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) => context.HttpContext.GetPrincipal().Scope == ApiScope.Release ? await next(context) : await AdminGuard.InvokeAsync(context, next); } /// 展示层守卫:站点公开时匿名可读;私有时需要登录或只读令牌。 public static class PublicGuard { public static async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { HttpContext http = context.HttpContext; if (!http.GetPrincipal().CanRead && !await http.RequestServices.GetRequiredService().IsSitePublicAsync(http.RequestAborted)) { return Problems.Create(http, MoShiroErrors.SitePrivate, "this site is private; sign in or use a read-only token"); } return await next(context); } /// 被隐藏(public_visible = false)的线路只对已认证的访问者可见。 public static bool CanSeeHidden(HttpContext http) => http.GetPrincipal().CanRead; }