Blazor Server(.NET8)注入服务认证状态为空问题排查
我正在开发首个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

