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

Blazor United(混合WebApp)用户初始化作用域服务异常:页面切换时服务重复初始化

Blazor United(混合WebApp)用户初始化作用域服务异常:页面切换时服务重复初始化

我看了你这段代码,核心问题出在Scoped服务的生命周期理解和用户身份初始化的实现方式上。你现在遇到的页面切换时UserService重复初始化,主要是因为当前的实现没有适配Blazor United混合模式的生命周期,加上直接在根组件里做初始化的逻辑有问题,咱们一步步拆解解决:

一、先搞懂为什么服务会重复初始化

你把UserService注册成了Scoped,但页面切换时它还是被重建,大概率是这两个原因:

  1. App.razor的渲染模式问题:如果你的App.razor是默认的静态渲染模式,那每次页面导航(静态渲染是类似MVC的请求模式)都会重建根组件,连带注入的Scoped服务也会跟着重建——静态渲染下Scoped服务的生命周期等价于“每次请求”,而非会话级。
  2. 初始化逻辑的位置不对:你在App.razor的OnInitializedAsync里调用InitUser,但根组件的初始化时机和页面导航强绑定,加上服务实例被重建,导致_initalized标记完全失效。

另外还有个隐藏问题:你直接修改AuthenticationState里的User对象,这种方式不符合Blazor授权系统的设计规范,很容易出现状态不一致的情况。

二、最优解决方案:用自定义AuthenticationStateProvider替代当前逻辑

你的核心需求是“用户登录后从数据库加载角色,补充到身份Claims中”,这个逻辑天生就该交给自定义AuthenticationStateProvider来做,而不是单独的Scoped服务。这样既能保证角色Claims在整个会话中生效,又能彻底解决重复初始化的问题。

步骤1:重构为自定义AuthenticationStateProvider

先把用户身份初始化的逻辑移到自定义的授权状态提供器里,它会自动在Blazor需要身份信息时触发,且整个Circuit生命周期内只会加载一次:

using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.EntityFrameworkCore;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    private readonly AuthenticationStateProvider _innerAuthProvider;
    private readonly IDbContextFactory<dbContext> _dbContextFactory;
    private AuthenticationState? _cachedAuthState;

    // 注入内置的授权提供器和数据库上下文工厂(别再直接new dbContext了)
    public CustomAuthStateProvider(AuthenticationStateProvider innerAuthProvider, IDbContextFactory<dbContext> dbContextFactory)
    {
        _innerAuthProvider = innerAuthProvider;
        _dbContextFactory = dbContextFactory;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        // 缓存状态,避免重复加载
        if (_cachedAuthState != null)
            return _cachedAuthState;

        // 获取原始的授权状态(比如SAML登录后的身份)
        var originalState = await _innerAuthProvider.GetAuthenticationStateAsync();
        var currentUser = originalState.User;

        // 只处理已认证的用户
        if (currentUser.Identity?.IsAuthenticated == true)
        {
            var userEmail = currentUser.Claims.FirstOrDefault(x => x.Type == "EmailAddress")?.Value;
            if (!string.IsNullOrEmpty(userEmail))
            {
                // 从数据库加载角色
                using var db = await _dbContextFactory.CreateDbContextAsync();
                var dbAccessControl = await db.access_control!.FirstOrDefaultAsync(x => x.email == userEmail);

                // 构建角色Claims
                var roleIdentity = new ClaimsIdentity();
                if (dbAccessControl?.roles == null || !dbAccessControl.roles.Any())
                {
                    roleIdentity.AddClaim(new Claim(ClaimTypes.Role, "None"));
                }
                else
                {
                    foreach (var role in dbAccessControl.roles)
                    {
                        roleIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
                    }
                }

                // 将角色身份添加到用户对象
                currentUser.AddIdentity(roleIdentity);
            }
        }

        // 缓存处理后的状态
        _cachedAuthState = new AuthenticationState(currentUser);
        return _cachedAuthState;
    }

    // 提供一个方法,当用户角色变化时可以手动刷新状态
    public void RefreshAuthState()
    {
        _cachedAuthState = null;
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }
}

步骤2:修改Program.cs的服务注册

更新服务注册,替换默认的授权状态提供器,同时改用IDbContextFactory来管理数据库上下文(避免连接泄漏):

using BlazorApp1.Client.Pages;
using BlazorApp1.Components;
using BlazorApp1.Components.User;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Sustainsys.Saml2;
using Sustainsys.Saml2.AspNetCore2;
using Sustainsys.Saml2.Metadata;

var builder = WebApplication.CreateBuilder(args);

// 1. 注册数据库上下文工厂(替换成你自己的连接字符串配置)
builder.Services.AddDbContextFactory<dbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// 2. 替换默认的授权状态提供器为我们自定义的
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();

// 3. 其他原有服务注册保持不变
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddInteractiveWebAssemblyComponents();

builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;
    options.Secure = CookieSecurePolicy.SameAsRequest;
    options.HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always;
});

builder.Services.AddAuthentication(sharedOptions =>
{
    sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    sharedOptions.DefaultChallengeScheme = Saml2Defaults.Scheme;
}).AddCookie()
  .AddSaml2();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});

builder.Services.AddSignalR();
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
builder.Services.AddHttpClient();
builder.Services.AddHttpContextAccessor();

// 如果还需要保留UserService来封装用户信息,就继续注册(后面会简化它)
builder.Services.AddScoped<UserService>();

var app = builder.Build();

// 中间件配置保持不变
if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

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

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(BlazorApp1.Client._Imports).Assembly);

app.Run();

步骤3:简化UserService(如果还需要的话)

如果还是需要一个UserService来封装用户信息,可以简化它,直接从自定义的授权状态提供器中获取数据,不需要自己做初始化:

using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;

public class UserService
{
    private readonly AuthenticationStateProvider _authStateProvider;
    private UserInfo? _cachedUserInfo;

    public UserService(AuthenticationStateProvider authStateProvider)
    {
        _authStateProvider = authStateProvider;
    }

    public async Task<UserInfo> GetCurrentUser()
    {
        if (_cachedUserInfo != null)
            return _cachedUserInfo;

        var authState = await _authStateProvider.GetAuthenticationStateAsync();
        var currentUser = authState.User;

        _cachedUserInfo = new UserInfo
        {
            Email = currentUser.Claims.FirstOrDefault(x => x.Type == "EmailAddress")?.Value,
            Roles = currentUser.Claims
                .Where(x => x.Type == ClaimTypes.Role)
                .Select(x => x.Value)
                .ToList()
        };

        return _cachedUserInfo;
    }

    // 当用户信息变化时清除缓存
    public void InvalidateUserCache()
    {
        _cachedUserInfo = null;
    }

    public class UserInfo
    {
        public string? Email { get; set; }
        public List<string>? Roles { get; set; }
    }
}

步骤4:修改App.razor,移除重复的初始化逻辑

现在不需要在App.razor里手动调用InitUser了,自定义的CustomAuthStateProvider会自动处理身份初始化。修改后的App.razor:

@using BlazorApp1.Components.User
@inject UserService UserService
@rendermode InteractiveServer <!-- 加上这个,确保根组件运行在Server模式下,Scoped服务生命周期绑定到Circuit -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="BlazorApp1.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet />
</head>
<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>
</html>

三、关键的注意事项

  1. 关于Scoped服务的生命周期:加上@rendermode InteractiveServer后,App.razor会运行在服务器端的Circuit中,Scoped服务的生命周期会绑定到整个Circuit(用户会话),页面切换时不会再重建服务。
  2. 不要直接new dbContext:用IDbContextFactory是Blazor中管理数据库上下文的标准方式,能避免连接泄漏和线程安全问题。
  3. 授权状态的一致性:通过自定义AuthenticationStateProvider提供的身份状态,会被Blazor的整个授权系统(比如[Authorize(Roles="xxx")]、AuthorizeView组件)自动识别,不会出现状态不一致的情况。
  4. 混合模式的适配:你的项目是混合模式(部分Server、部分WebAssembly),角色Claims在服务器端添加后,会通过SignalR自动同步到客户端的WebAssembly组件中,不需要在客户端重复处理。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 06:55:29