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

Blazor .NET 8 InteractiveServer模式下本地AD认证异常问题

Blazor InteractiveServer模式下AD认证用户信息二次渲染丢失问题

问题现象

开发基于本地AD(Active Directory)认证的Blazor应用,启用InteractiveServer渲染模式时出现异常:

  • 页面先执行服务器预渲染,再进入客户端交互渲染
  • 第一次触发OnInitializedAsync()时能正确获取用户AD信息与角色权限
  • 第二次触发方法时用户身份变为未认证状态,导致权限控制的内容被隐藏
  • 使用普通用户账号模板测试功能正常,推测客户端未正确同步用户认证状态

已尝试操作

在App.Razor和页面分别设置InteractiveServer渲染模式,问题未解决


相关代码

Program.cs 核心配置

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
builder.Services.AddServerSideBlazor();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddTransient<IPersonSvc, PersonSvc>();
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
    .AddNegotiate(options =>
    {
        options.Events = new NegotiateEvents
        {
            OnAuthenticated = context =>
            {
                var windowsIdentity = context.Principal.Identity as WindowsIdentity;
                var adGroups = windowsIdentity?.Groups.Translate(typeof(NTAccount));

                if (adGroups != null)
                {
                    foreach (var group in adGroups)
                    {                       
                        context.Principal.Identities.FirstOrDefault().AddClaim(new Claim(ClaimTypes.Role, group.Value));
                    }
                }

                return Task.CompletedTask;
            },
        };
    }).AddIdentityCookies();

builder.Services.Configure<AuthenticationOptions>(options =>
{
    options.DefaultScheme = NegotiateDefaults.AuthenticationScheme;
});
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("No connection string in config.");

builder.Services.AddDbContextFactory<ApplicationDbContext>((DbContextOptionsBuilder options) => 
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorComponents(options => options.DetailedErrors = true);
builder.Services.AddTelerikBlazor();
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

页面代码(/PhoneDirectory)

@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IPersonSvc PersonService
@attribute [Authorize]
@rendermode InteractiveServer
@page "/PhoneDirectory"

Page Content here
@code {

    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    List<Person>? GridData;

    private bool ViewAdditionalInfo { get; set; }
    private bool isInitialized = false;
    private ClaimsPrincipal user;
    
   protected override async Task OnInitializedAsync()
   {
       if (!isInitialized)
       {
           var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
           user = authenticationState.User;
           // 第一次有用户信息,第二次未认证且isInitialized重置为false

           await LoadData();
           isInitialized = true;
       }
      

       await base.OnInitializedAsync();
   }
}

App.Razor 代码

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <script src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js" defer></script>
    <link href="bootstrap/css/bootstrap.min.css" rel="stylesheet" />
    <link href="/bootstrap-icons/font/bootstrap-icons.min.css" rel="stylesheet" />
    <link rel="stylesheet" href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" />
    <link rel="stylesheet" href="css/app.css" />
    <link rel="stylesheet" href="InternalSite.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet />
</head>
<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>

问题根源与解决方案

1. 问题根源

InteractiveServer模式的双渲染机制导致:

  • 预渲染阶段:HttpContext可用,能获取AD认证信息,但组件状态不会自动传递到交互阶段
  • 交互阶段:SignalR连接建立后,原预渲染的组件状态重置,且直接调用AuthenticationStateProvider.GetAuthenticationStateAsync()无法获取AD认证上下文
  • 页面中同时使用AuthenticationStateProvider注入和级联AuthenticationState,逻辑混乱;HttpContext仅在预渲染阶段可用,交互阶段为空

2. 分步解决方案

方案一:禁用预渲染(快速临时解决)

直接关闭预渲染,确保仅进入交互渲染阶段:

  • 页面级修改:将@rendermode InteractiveServer改为@rendermode InteractiveServer(prerender: false)
  • 全局修改:在Program.cs中更新映射配置:
    app.MapRazorComponents<App>()
        .AddInteractiveServerRenderMode(options => options.DisablePrerendering = true);
    
    此方法简单,但会损失预渲染的首屏加载速度优势。

方案二:正确传递认证状态到交互阶段(推荐)

步骤1:配置Program.cs确保认证状态传递

在AddInteractiveServerComponents()后添加SignalR连接的认证传递配置:

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddHubOptions(options =>
    {
        options.MaximumReceiveMessageSize = 1024 * 1024 * 10;
    });

确保AddCascadingAuthenticationState()已正确注册,它负责在组件树中传递认证状态。

步骤2:优化页面代码,使用级联认证状态

移除AuthenticationStateProvider注入和HttpContext级联参数,统一使用级联的AuthenticationStateTask:

@inject IPersonSvc PersonService
@attribute [Authorize]
@rendermode InteractiveServer
@page "/PhoneDirectory"

Page Content here
@code {
    [CascadingParameter]
    private Task<AuthenticationState> AuthenticationStateTask { get; set; } = default!;

    List<Person>? GridData;
    private bool ViewAdditionalInfo { get; set; }
    private bool isInitialized = false;
    private ClaimsPrincipal user;
       
    protected override async Task OnInitializedAsync()
    {
        if (!isInitialized)
        {
            var authState = await AuthenticationStateTask;
            user = authState.User;
            
            if (user.Identity?.IsAuthenticated == true)
            {
                await LoadData();
            }
            isInitialized = true;
        }

        await base.OnInitializedAsync();
    }
}
步骤3:更新App.Razor,添加级联认证状态包裹

将Routes组件用CascadingAuthenticationState包裹,确保认证状态传递到所有子组件:

<head>
    <!-- 原有head内容不变 -->
</head>
<body>
    <CascadingAuthenticationState>
        <Routes />
    </CascadingAuthenticationState>
    <script src="_framework/blazor.web.js"></script>
</body>

3. 额外注意事项

  • 避免在交互阶段依赖HttpContext,所有认证操作通过AuthenticationState处理
  • 测试前清除浏览器缓存,确保认证Cookie正确传递到SignalR连接
  • 移除页面中重复的认证相关注入,保持逻辑统一

内容的提问来源于stack exchange,提问作者Ryan O

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 09:08:11