You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于请求头的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实例,彻底打破了用户身份的隔离性。

正确的实现方案

应该基于每个请求的上下文提取用户信息,而非全局共享,具体步骤如下:

  1. 注册自定义认证方案
    使用官方认证框架,添加从请求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));
        }
    }
    
  2. 调整中间件顺序
    必须按正确顺序添加中间件,否则认证授权逻辑不会生效:

    app.UseAuthentication();
    app.UseAuthorization();
    
    app.UseBlazorFrameworkFiles();
    app.UseStaticFiles();
    app.UseRouting();
    
    app.MapRazorPages();
    app.MapControllers();
    app.MapFallbackToFile("index.html");
    
  3. 无需自定义AuthenticationStateProvider
    配置好官方认证框架后,Blazor Server默认的ServerAuthenticationStateProvider会自动从HttpContext中获取认证后的用户信息,直接支持AuthorizeAttribute和CascadingAuthenticationState的使用。

额外注意事项

  • 确保网关的认证拦截
    必须确认F5网关已拦截所有未认证请求,不允许未携带正确Header的请求进入Blazor应用,避免出现匿名用户访问的情况。
  • SignalR连接的身份复用
    Blazor Server的SignalR长连接会自动复用初始请求的认证信息,只要初始请求通过网关验证,后续交互都会保持正确的用户身份,无需额外处理。

内容的提问来源于stack exchange,提问作者Palo Mraz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 22:25:54