基于请求头的Blazor Server自定义认证方案是否合规?
我有一个部署在F5网关后的Blazor Server应用,仅允许已认证请求访问该应用,网关通过自定义HTTP请求头传递用户信息。我希望将已认证用户信息与Blazor Server的授权功能集成,设计了一套看似可行的简单方案:
在Program.cs启动代码中,我注册了一个内联中间件,从请求自定义头初始化声明集合,并基于该集合实现了一个继承自AuthenticationStateProvider的CustomAuthenticationStateProvider类:
// Set of claims that are initialized in the inline middleware below from request headers and // used to create the user's identity through the CustomAuthenticationStateProvider List<Claim> claimsFromRequestHeaders = new(); // Add the CustomAuthenticationStateProvider to the service collection with a delegate that closes // over the claimsFromRequestHeaders initialized by the inline middleware below. builder.Services.AddScoped<AuthenticationStateProvider>(implementationFactory: (serviceProvider) => { return new CustomAuthenticationStateProvider(claimsFromRequestHeaders); }); var app = builder.Build(); // Inline middleware that initializes the claimsFromRequestHeaders collection from the first request. app.Use(async (context, next) => { // Do not initialize more than once. if (claimsFromRequestHeaders.Count > 0) { await next(); return; } // This would be initialized from request headers... if (context.Request.Headers.TryGetValue("X-Claim-UserId", out var userId)) { claimsFromRequestHeaders.Add(new Claim(ClaimTypes.NameIdentifier, userId!)); } if (context.Request.Headers.TryGetValue("X-Claim-UserName", out var userName)) { claimsFromRequestHeaders.Add(new Claim(ClaimTypes.Name, userName!)); } await next(); });
该CustomAuthenticationStateProvider实现非常简洁:
/// <summary> /// Creates an <see cref="AuthenticationState"/> with claims provider in constructor. /// </summary> public class CustomAuthenticationStateProvider : AuthenticationStateProvider { private readonly IReadOnlyCollection<Claim> _claims; public CustomAuthenticationStateProvider(IReadOnlyCollection<Claim> claims) { this._claims = claims; } public override Task<AuthenticationState> GetAuthenticationStateAsync() { var identity = new ClaimsIdentity(this._claims, "Custom Authentication"); var user = new ClaimsPrincipal(identity); return Task.FromResult(new AuthenticationState(user)); } }
通过此设置,我可以使用AuthorizeAttribute进行页面级访问控制,也能通过CascadingAuthenticationState获取用户信息。应用采用InteractiveServer渲染模式,启动代码中未调用AddAuthentication/AddAuthorization及UseAuthentication/UseAuthorization,但运行符合预期。
我想请教该方案是否正确,是否存在遗漏点?
你的方案能实现基础的授权和用户信息获取,但存在致命的生产环境风险,核心问题在于违背了Blazor Server的服务生命周期和多用户隔离设计:
全局共享Claims集合导致用户身份串流
你在Program.cs中声明的claimsFromRequestHeaders是全局变量,所有请求都会复用这个集合。第一个请求初始化后,后续所有用户的请求都会使用第一个用户的Claims,直接导致不同用户的身份混乱。中间件仅初始化一次Claims
中间件逻辑只要集合不为空就不再处理后续请求的Header,这意味着只有第一个访问的用户信息会被保留,后续所有用户都会被迫使用该用户的身份,完全不符合多用户系统的要求。错误绑定Scoped服务与全局变量
Blazor Server中AuthenticationStateProvider是Scoped生命周期(每个用户会话对应一个实例),但你用全局Claims集合绑定所有Provider实例,彻底打破了用户身份的隔离性。
正确的实现方案
应该基于每个请求的上下文提取用户信息,而非全局共享,具体步骤如下:
注册自定义认证方案
使用官方认证框架,添加从请求Header提取Claims的自定义Handler,确保每个请求都能正确获取当前用户的身份:builder.Services.AddAuthentication("CustomHeaderAuth") .AddScheme<AuthenticationSchemeOptions, CustomHeaderAuthenticationHandler>( "CustomHeaderAuth", options => {}); builder.Services.AddAuthorization(options => { // 设置默认策略,强制所有请求需要认证 options.FallbackPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); });自定义认证Handler实现:
public class CustomHeaderAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { public CustomHeaderAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { var claims = new List<Claim>(); if (Context.Request.Headers.TryGetValue("X-Claim-UserId", out var userId)) { claims.Add(new Claim(ClaimTypes.NameIdentifier, userId!)); } if (Context.Request.Headers.TryGetValue("X-Claim-UserName", out var userName)) { claims.Add(new Claim(ClaimTypes.Name, userName!)); } // 如果网关已确保所有请求都有认证Header,这里可以直接返回成功;否则需处理匿名情况 var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return Task.FromResult(AuthenticateResult.Success(ticket)); } }调整中间件顺序
必须按正确顺序添加中间件,否则认证授权逻辑不会生效:app.UseAuthentication(); app.UseAuthorization(); app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); app.MapRazorPages(); app.MapControllers(); app.MapFallbackToFile("index.html");无需自定义AuthenticationStateProvider
配置好官方认证框架后,Blazor Server默认的ServerAuthenticationStateProvider会自动从HttpContext中获取认证后的用户信息,直接支持AuthorizeAttribute和CascadingAuthenticationState的使用。
额外注意事项
- 确保网关的认证拦截
必须确认F5网关已拦截所有未认证请求,不允许未携带正确Header的请求进入Blazor应用,避免出现匿名用户访问的情况。 - SignalR连接的身份复用
Blazor Server的SignalR长连接会自动复用初始请求的认证信息,只要初始请求通过网关验证,后续交互都会保持正确的用户身份,无需额外处理。
内容的提问来源于stack exchange,提问作者Palo Mraz

