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

.NET 8 Blazor Web App登录异常求助:控制器登录成功但AuthorizeView未生效

问题分析与解决方案

问题背景

使用.NET 8 Blazor Web App框架,基于Microsoft.AspNetCore.Identity.EntityFramework实现认证。因InteractiveServer渲染模式不支持直接使用HttpContext/SignInManager/UserManager,通过向控制器发送JSON请求实现登录功能,注册功能已成功实现(未包含自动登录),但登录后AuthorizeView仍显示未登录状态。断点调试确认控制器中SignInManager.PasswordSignInAsync返回登录成功,尝试自定义AuthenticationStateProvider更新认证状态也未解决问题。

现有代码片段

Razor登录页面代码

@inject IHttpClientFactory _HttpClientFactory
@inject IStringLocalizer<Login> _Localizer
@inject NavigationManager _NavigationManager
@inject IMessageService _Message

<EditForm Model="_Input" method="post" OnValidSubmit="LoginUser" FormName="login" Enhance class="Flex">
    <DataAnnotationsValidator />
    <div id="SubmitDiv" class="FlexAll">
        <InputText @bind-Value="_Input.Email" class="ant-input SubmitContentInput" autocomplete="username" aria-required="true" placeholder="@_Localizer["ErrorMessageUserNameRequire"]" />
        <ValidationMessage For="() => _Input.Email" class="ant-form-item-explain-error" />
        <InputText @bind-Value="_Input.Password" type="password" class="ant-input SubmitContentInput" autocomplete="current-password" aria-required="true" placeholder="@_Localizer["ErrorMessagePasswordRequire"]" />
        <ValidationMessage For="() => _Input.Password" class="ant-form-item-explain-error" />
    </div>    
    <button type="submit" class="ant-btn ant-btn-primary Flex">@_Localizer["Login"]</button>
</EditForm>

[SupplyParameterFromForm]
private LoginModel _Input { get; set; } = new();

public async Task LoginUser()
{
    var httpClient = _HttpClientFactory.CreateClient();
    var httpResponseMessage = await httpClient.PostAsJsonAsync<LoginModel>($"{_NavigationManager.BaseUri}Login/Login", this._Input);

    if (httpResponseMessage.IsSuccessStatusCode)
    {
        var result = await httpResponseMessage.Content.ReadFromJsonAsync<LoginResultModel>();
        if (result is { })
        {
            switch (result.LoginResultType)
            {
                case LoginResultModel.LoginResultTypeEnum.Success:
                    {
                        _NavigationManager.NavigateTo("/");
                        return;
                    }                
            }
        }
        else
        {
            await _Message.Error(_Localizer["UnableToLogInNormally"].Value);
        }
    }
}

登录控制器代码

[Microsoft.AspNetCore.Mvc.Route("[controller]/[action]")]
public class LoginController: Controller
{
    readonly SignInManager<ApplicationUser> _SignInManager;
    readonly ILogger<Login> _Logger;        

    public LoginController(SignInManager<ApplicationUser> signInManager, ILogger<Login> logger)
    { 
        _SignInManager = signInManager;
        _Logger = logger;
    }

    [HttpPost]
    [Produces("application/json")]
    public async Task<ActionResult<LoginResultModel>> Login([FromBody] LoginModel login)
    {
        LoginResultModel _loginResult = new LoginResultModel();
        var result = await _SignInManager.PasswordSignInAsync(login.Email, login.Password, login.RememberMe, lockoutOnFailure: false);
        
        if (result.Succeeded)
        {
            _Logger.LogInformation("User logged in.");
            _loginResult.LoginResultType = LoginResultModel.LoginResultTypeEnum.Success;
            return _loginResult;
        }
        else if (result.RequiresTwoFactor)
        {
            _loginResult.LoginResultType = LoginResultModel.LoginResultTypeEnum.RequiresTwoFactor;
            _loginResult.QueryParameter = new() { ["rememberMe"] = login.RememberMe };
            return _loginResult;               
        }
        else if (result.IsLockedOut)
        {
            _loginResult.LoginResultType = LoginResultModel.LoginResultTypeEnum.LockedOut;                
            return _loginResult;
        }
        else
        {
            _loginResult.LoginResultType = LoginResultModel.LoginResultTypeEnum.InvalidLoginAttempt;
            return _loginResult;
        }
    }
}

AuthorizeView代码

<AuthorizeView>
    <Authorized>
        <div class="page">
            <div class="sidebar">
                <NavMenu />
            </div>

            <main>
                <div class="top-row px-4">
                    <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">@_Localizer["Test"]</a>
                </div>
                <article class="content px-4">
                    @Body
                </article>
            </main>
        </div>
    </Authorized>
    <NotAuthorized>
        <Login></Login>
    </NotAuthorized>
</AuthorizeView>

自定义AuthenticationStateProvider代码

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {            
        var _user = new ClaimsPrincipal(new ClaimsIdentity());
        return Task.FromResult(new AuthenticationState(_user));
    }

    public void MarkUserAsAuthored(string eMail)
    {
        var _identity= new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name,eMail) }, "apiauth_type");
        var _user = new ClaimsPrincipal(_identity);
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_user)));
    }
}

问题根源

  1. 认证状态不同步:
    控制器中SignInManager.PasswordSignInAsync成功后,仅向HTTP响应写入认证Cookie,但Blazor InteractiveServer的SignalR连接独立于HTTP请求,当前已建立的连接不会自动读取新生成的Cookie,因此AuthorizeView仍使用连接建立时的未认证状态。

  2. 自定义AuthenticationStateProvider的缺陷:

    • 控制器调用MarkUserAsAuthored无效:控制器与Blazor组件处于不同依赖注入作用域,二者的AuthenticationStateProvider实例不共享,控制器的调用无法通知到Blazor组件的状态监听。
    • 自定义Provider默认返回匿名用户,未读取Identity认证Cookie,即使临时修改状态,页面刷新后也会恢复为未认证状态。

解决方案

方案一:强制页面刷新(快速解决)

登录成功后,使用forceLoad: true触发全页面刷新,重新发起HTTP请求并携带登录后的认证Cookie,Blazor会重新建立SignalR连接并获取正确的认证状态:

case LoginResultModel.LoginResultTypeEnum.Success:
    {
        _NavigationManager.NavigateTo("/", forceLoad: true);
        return;
    }

方案二:正确实现自定义AuthenticationStateProvider

  1. 注册自定义Provider:
    在Program.cs中替换默认的AuthenticationStateProvider:

    builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
    
  2. 修改自定义Provider,读取认证Cookie:
    注入IHttpContextAccessor读取当前请求的认证Cookie,确保初始状态正确:

    public class CustomAuthenticationStateProvider : AuthenticationStateProvider
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
    
        public CustomAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }
    
        public override Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var user = _httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(new ClaimsIdentity());
            return Task.FromResult(new AuthenticationState(user));
        }
    
        public void MarkUserAsAuthored(ClaimsPrincipal user)
        {
            NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
        }
    }
    
  3. 在Razor页面中更新状态:
    登录成功后,调用自定义Provider的方法更新状态:

    // 在LoginUser方法的Success分支
    case LoginResultModel.LoginResultTypeEnum.Success:
        {
            var authStateProvider = (CustomAuthenticationStateProvider)_AuthenticationStateProvider;
            authStateProvider.MarkUserAsAuthored(new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, _Input.Email) }, "apiauth_type")));
            _NavigationManager.NavigateTo("/");
            return;
        }
    

方案三:使用Blazor内置认证流程

若业务允许,可切换到InteractiveWebAssembly模式,或使用Blazor的RemoteAuthenticationService处理认证,避免手动控制器登录的状态同步问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 21:48:12