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

Blazor Server(.NET8)注入服务认证状态为空问题排查

Blazor Server(.NET 8)认证信息注入服务为空及IIS部署问题

我正在开发首个Blazor Server端应用(.NET 8),遇到认证逻辑问题:注入到服务中的用户/认证信息为空。

我编写了一个用于存储登录后用户信息的简单UserService:

public class UserService
{
    private string _username = string.Empty;
    public string Username => _username;
    public bool IsAuthenticated => !string.IsNullOrEmpty(_username);

    public void SetUser(string username)
    {
        _username = username;
    }

    public void ClearUser()
    {
        _username = "";
    }
}

但当它被注入到TestService时,Username始终为空:

public class TestService
{
    private string Username { get; set; }

    public TestService(UserService userService)
    {
        Username = userService.Username; // <-- Username is empty here.
    }
}

登录页面(Login.cshtml.cs)、自定义AuthenticationStateProvider、Program.cs的配置如下:

Login.cshtml.cs

public class LoginModel : PageModel
{
    private readonly LoginService _loginService;
    private readonly CustomAuthenticationStateProvider _authenticationStateProvider;

    [BindProperty]
    public string Username { get; set; }

    [BindProperty]
    public string Password { get; set; }

    public string ErrorMessage { get; set; }
    public string ReturnUrl { get; set; } = "/";

    public LoginModel(LoginService loginService, CustomAuthenticationStateProvider authenticationStateProvider)
    {
        _loginService = loginService;
        _authenticationStateProvider = authenticationStateProvider;
    }

    public void OnGet(string? returnUrl = "/")
    {
        ReturnUrl = returnUrl ?? "/";
    }

    public async Task<IActionResult> OnPostAsync(string returnUrl = "/")
    {
        if (_loginService.CheckCredentials(Username, Password))
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, Username)
            };

            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
            var principal = new ClaimsPrincipal(identity);

            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);

            _authenticationStateProvider.NotifyUserAuthentication(Username); // <-- Username is set here.

            if (Url.IsLocalUrl(returnUrl))
                return Redirect(returnUrl);
            
            return Redirect("/");
            
        }
        else
        {
            ErrorMessage = "Invalid username or password";
            return Page();
        }
    }
}

CustomAuthenticationStateProvider

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly UserService _userService;
    private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());

    public CustomAuthenticationStateProvider(UserService userService)
    {
        _userService = userService;
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        if (!_userService.IsAuthenticated)
        {
            return Task.FromResult(new AuthenticationState(_anonymous));
        }

        var claims = new[] { new Claim(ClaimTypes.Name, _userService.Username) };  // <-- _userService.Username is set here.
        var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        var user = new ClaimsPrincipal(identity);

        return Task.FromResult(new AuthenticationState(user)); 
    }

    public void NotifyUserAuthentication(string username)
    {
        _userService.SetUser(username); // <-- username is set here.
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public void NotifyUserLogout()
    {
        _userService.ClearUser();
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }
}

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddDevExpressBlazor(options => {
    options.BootstrapVersion = DevExpress.Blazor.BootstrapVersion.v5;
    options.SizeMode = DevExpress.Blazor.SizeMode.Medium;
});

builder.Services.AddMvc();
builder.Services.AddRazorPages();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<LoginService>();
builder.Services.AddScoped<TestService>();
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddBlazoredSessionStorage();

builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Login"; 
        options.ExpireTimeSpan = TimeSpan.FromMinutes(30); 
        options.SlidingExpiration = true; 
        options.Cookie.IsEssential = true;
    });


builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<CustomAuthenticationStateProvider>());

builder.Services.AddAuthorizationCore();

var app = builder.Build();
app.UseSession();

app.UseHttpsRedirection();

app.UseStaticFiles(); ;
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.UseAntiforgery();

app.MapControllers();
app.MapRazorPages();

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

app.Run();

用户登录时,用户名能正确传递到CustomAuthenticationStateProvider的NotifyUserAuthentication方法(该方法会调用_userService.SetUser),且GetAuthenticationStateAsync中_userService.Username已设置,但仅在TestService的构造函数内Username为空。

更新信息:

  • 改用IHttpContextAccessor在开发环境可正常获取用户信息,但部署到IIS后,通过HttpContext获取的用户信息仍为null。

解决方案

1. 解决TestService构造函数中UserService值为空的问题

TestService是Scoped服务,其构造函数在依赖注入容器创建实例时执行——此时用户还未登录,UserService的Username自然为空。你不应该在构造函数中直接获取Username,而是在需要使用时实时访问UserService:

修改TestService:

public class TestService
{
    private readonly UserService _userService;

    // 保留对UserService的引用,而不是在构造函数中读取值
    public TestService(UserService userService)
    {
        _userService = userService;
    }

    // 需要用户名时再实时获取
    public string GetCurrentUsername()
    {
        return _userService.Username;
    }
}

这样在用户登录后调用GetCurrentUsername()时,就能拿到最新的用户名。

2. 解决IIS部署后HttpContextAccessor获取用户为空的问题

原因分析

Blazor Server使用SignalR连接处理交互,而HttpContext仅存在于初始请求阶段。在IIS部署时,可能因为Cookie配置或SignalR连接的上下文传递问题,导致后续SignalR请求中无法正确获取认证信息。

修复步骤

步骤1:调整Cookie认证配置

修改Program.cs中Cookie认证配置,确保Cookie能跨SignalR连接传递:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Login";
        options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
        options.SlidingExpiration = true;
        options.Cookie.IsEssential = true;
        // 允许Cookie跨上下文传递
        options.Cookie.SameSite = SameSiteMode.Lax;
        // 生产环境启用HTTPS时设置为Always
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        // 确保Cookie在SignalR连接中可用
        options.Cookie.HttpOnly = false;
    });

步骤2:改用AuthenticationStateProvider获取认证状态(推荐)

放弃直接使用IHttpContextAccessor,改用Blazor官方推荐的AuthenticationStateProvider,这是Blazor Server中获取认证状态的标准方式:

修改TestService:

public class TestService
{
    private readonly AuthenticationStateProvider _authStateProvider;

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

    public async Task<string> GetCurrentUsername()
    {
        var authState = await _authStateProvider.GetAuthenticationStateAsync();
        return authState.User.Identity?.Name ?? string.Empty;
    }
}

步骤3:检查IIS站点配置

  • 确保站点启用SSL(如果使用HTTPS)。
  • 应用程序池的**.NET CLR版本**设置为.NET 8。
  • 禁用IIS的匿名身份验证,确保表单身份验证生效。

步骤4:验证中间件顺序

确保Program.cs中的中间件顺序正确:

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// 顺序:Session -> Authentication -> Authorization
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();

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

app.Run();

3. 统一认证状态同步逻辑

当前你的CustomAuthenticationStateProvider依赖UserService存储用户信息,同时又调用HttpContext.SignInAsync,可能导致状态不一致。建议统一通过AuthenticationStateProvider管理:

简化CustomAuthenticationStateProvider:

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());

    public CustomAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        // 直接从HttpContext获取初始请求的认证状态
        var context = _httpContextAccessor.HttpContext;
        if (context?.User?.Identity?.IsAuthenticated ?? false)
        {
            return new AuthenticationState(context.User);
        }

        // 对于SignalR后续请求,从基础方法恢复状态
        var authState = await base.GetAuthenticationStateAsync();
        return authState.User.Identity?.IsAuthenticated == true ? authState : new AuthenticationState(_anonymous);
    }

    public void NotifyUserAuthentication(ClaimsPrincipal principal)
    {
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(principal)));
    }

    public void NotifyUserLogout()
    {
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_anonymous)));
    }
}

修改LoginModel的OnPostAsync方法:

public async Task<IActionResult> OnPostAsync(string returnUrl = "/")
{
    if (_loginService.CheckCredentials(Username, Password))
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, Username)
        };

        var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        var principal = new ClaimsPrincipal(identity);

        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);

        // 直接传递ClaimsPrincipal同步状态
        _authenticationStateProvider.NotifyUserAuthentication(principal);

        if (Url.IsLocalUrl(returnUrl))
            return Redirect(returnUrl);
        
        return Redirect("/");
        
    }
    else
    {
        ErrorMessage = "Invalid username or password";
        return Page();
    }
}

这样可以避免状态不一致问题,更符合Blazor认证设计规范。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 08:33:10