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