登录 Blazor 服务器端应用程序不起作用

use*_*480 7 c# blazor-server-side asp.net-core-3.0

我正在为 Asp.net 核心 3.0 Blazor 服务器端应用程序构建示例登录 razor 组件。每当代码到达 SignInAsyc 方法时,它似乎只是挂起或锁定,因为代码停止进一步执行。我还尝试通过使用 PasswordSignInAsync 方法来切换逻辑,这给了我完全相同的结果。所有代码都将在该方法之前执行,但在执行该语句时会冻结。我在这里缺少什么?

Razor 组件页面:

<div class="text-center">
    <Login FieldsetAttr="fieldsetAttr" UsernameAttr="usernameAttr" PasswordAttr="passwordInput"
           ButtonAttr="buttonAttr" ButtonText="Sign In" InvalidAttr="invalidAttr" />

</div>

@code {
    Dictionary<string, object> fieldsetAttr =
        new Dictionary<string, object>()
        {
            {"class", "form-group" }
        };

    Dictionary<string, object> usernameAttr =
        new Dictionary<string, object>()
        {
            {"class", "form-control" },
            {"type", "text" },
            {"placeholder", "Enter your user name here." }
        };

    Dictionary<string, object> passwordInput =
        new Dictionary<string, object>()
        {
            {"class", "form-control" },
            {"type", "password" }
        };

    Dictionary<string, object> buttonAttr =
        new Dictionary<string, object>()
        {
            {"type", "button" }
        };

    Dictionary<string, object> invalidAttr =
        new Dictionary<string, object>()
        {
            {"class", "" },
            {"style", "color: red;" }
        };

    Dictionary<string, object> validAttr =
        new Dictionary<string, object>()
        {
            {"class", "" },
            {"style", "color: green;" }
        };

}
Run Code Online (Sandbox Code Playgroud)

剃须刀组件:

@inject SignInManager<IdentityUser> signInManager
@inject UserManager<IdentityUser> userManager

<div @attributes="FormParentAttr">
    <form @attributes="LoginFormAttr">
        <fieldset @attributes="FieldsetAttr">
            <legend>Login</legend>
            <label for="usernameId">Username</label><br />
            <input @attributes="UsernameAttr" id="usernameId" @bind="UserName" /><br />
            <label for="upasswordId">Password</label><br />
            <input @attributes="PasswordAttr" id="passwordId" @bind="Password" /><br />
            <button @attributes="ButtonAttr" @onclick="@(async e => await LoginUser())">@ButtonText</button>
            @if (errorMessage != null && errorMessage.Length > 0)
            {
                <div @attributes="InvalidAttr">
                    @errorMessage
                </div>
            }
            else if(successMessage != null && successMessage.Length > 0)
            {
                <div @attributes="ValidAttr">
                    @successMessage
                </div>
            }
        </fieldset>
    </form>
</div>

@code {

    string successMessage = "";

    private async Task LoginUser()
    {
        if(!String.IsNullOrEmpty(UserName))
        {
            var user = await userManager.FindByNameAsync(UserName);
            var loginResult =
                await signInManager.CheckPasswordSignInAsync(user, Password, false);



            if(loginResult.Succeeded)
            {
                await signInManager.SignInAsync(user, true);
                successMessage = $"{UserName}, signed in.";
                errorMessage = "";
            }
            else
            {
                successMessage = "";
                errorMessage = "Username or password is incorrect.";
            }
        }
        else
        {
            successMessage = "";
            errorMessage = "Provide a username.";
        }
    }

    [Parameter]
    public Dictionary<string, object> FormParentAttr { get; set; }

    [Parameter]
    public Dictionary<string, object> LoginFormAttr { get; set; }

    [Parameter]
    public Dictionary<string, object> FieldsetAttr { get; set; }

    [Parameter]
    public Dictionary<string, object> UsernameAttr { get; set; }

    [Parameter]
    public Dictionary<string, object> PasswordAttr { get; set; }

    [Parameter]
    public Dictionary<string,object> ButtonAttr { get; set; }

    [Parameter]
    public Dictionary<string, object> InvalidAttr { get; set; }

    private string UserName { get; set; }
    private string Password { get; set; }

    [Parameter]
    public string ButtonText { get; set; }

    [Parameter]
    public Dictionary<string, object> ValidAttr { get;set; }

    public string errorMessage { get; set; }

}
Run Code Online (Sandbox Code Playgroud)

Jam*_*aug 11

itminus 之前的答案以及评论中讨论的问题之一是在手动刷新、会话结束或导致刷新的链接后保持用户的状态。这会丢失用户的状态,因为 cookie 值没有设置到客户端的浏览器,这意味着下一个 HTTP 请求不包含 cookie。一种解决方案是使用静态登录/退出页面,这将允许将 cookie 发送到客户端的浏览器。

此方法改为使用 JS 将 cookie 写入客户端浏览器,从而允许 Blazor 处理所有事情。我遇到了 cookie 设置未正确设置的一些问题,因为我误解了如何AddCookie()在启动中将选项添加到 DI 容器。它使用IOptionsMonitor来使用命名选项,并使用Scheme作为键。

我已经修改了登录代码以调用将保存 cookie 的 JS。您可以在注册新用户或登录现有用户后运行此命令。

确保您 DI IOptionsMonitor<CookieAuthenticationOptions>,允许您使用方案作为键来解析命名选项。确保您使用.Get(schemeName)而不是.CurrentValue,否则您TicketDataFormat(和其他设置)将不正确,因为它将使用默认值。我花了几个小时才意识到这一点。

注意:IOptionsMonitor<CookieAuthenticationOptions>来自调用services.AddAuthentication().AddCookie(). 下面提供了一个示例。

    _cookieAuthenticationOptions = cookieAuthenticationOptionsMonitor.Get("MyScheme");
    ...
    private async Task SignInAsync(AppUser user, String password)
    {
        //original code from above answer
        var principal = await _signInManager.CreateUserPrincipalAsync(user);

        var identity = new ClaimsIdentity(
            principal.Claims,
            "MyScheme"
        );
        principal = new ClaimsPrincipal(identity);
        _signInManager.Context.User = principal;
        _hostAuthentication.SetAuthenticationState(Task.FromResult(new AuthenticationState(principal)));

        // this is where we create a ticket, encrypt it, and invoke a JS method to save the cookie
        var ticket = new AuthenticationTicket(principal, null, "MyScheme");
        var value = _cookieAuthenticationOptions.TicketDataFormat.Protect(ticket);
        await _jsRuntime.InvokeVoidAsync("blazorExtensions.WriteCookie", "CookieName", value, _cookieAuthenticationOptions.ExpireTimeSpan.TotalDays);
    }
Run Code Online (Sandbox Code Playgroud)

然后我们编写一个 JS cookie

    window.blazorExtensions = {

        WriteCookie: function (name, value, days) {

            var expires;
            if (days) {
                var date = new Date();
                date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
                expires = "; expires=" + date.toGMTString();
            }
            else {
                expires = "";
            }
            document.cookie = name + "=" + value + expires + "; path=/";
        }
    }
Run Code Online (Sandbox Code Playgroud)

这将成功地将cookie写入客户端的浏览器。如果您遇到问题,请确保您的启动使用相同的方案名称。如果不这样做,那么普通的 cookie 身份验证系统将无法正确解析回已编码的主体:

        services.AddIdentityCore<AppUser>()
            .AddRoles<IdentityRole>()
            .AddEntityFrameworkStores<AppDbContext>()
            .AddSignInManager();

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "MyScheme";
        }).AddCookie("MyScheme", options =>
        {
            options.Cookie.Name = "CookieName";
        });
Run Code Online (Sandbox Code Playgroud)

对于完成主义者,也可以用同样的方式实现注销:

    private async Task SignOutAsync()
    {
        var principal = _signInManager.Context.User = new ClaimsPrincipal(new ClaimsIdentity());
        _hostAuthentication.SetAuthenticationState(Task.FromResult(new AuthenticationState(principal)));

        await _jsRuntime.InvokeVoidAsync("blazorExtensions.DeleteCookie", _appInfo.CookieName);

        await Task.CompletedTask;
    }
Run Code Online (Sandbox Code Playgroud)

还有JS:

    window.blazorExtensions = {
        DeleteCookie: function (name) {
            document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:01 GMT";
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • 我很害怕知道即使在 .net 5 上我们也没有或者我找不到登录 blazor 服务器的简单解决方案,我也会尝试实现这一点。 (4认同)

itm*_*nus 10

基本上,这是因为在SigninManger::SignInAsync()实际上将尝试发送一个cookieHTTP以表示该用户在已签署的。但在这一刻Blazor服务器端处理的时候,有没有可用的HTTP响应,则各只有一个WebSocket连接(SignalR)。

怎么修

简而言之,Signin 是持久化用户凭据/cookies/... 以便 WebApp 知道客户端是谁。由于您使用的是 Blazor 服务器端,因此您的客户端正在通过WebSocket 连接与服务器通信。无需通过 发送 cookie HTTP。因为你的 WebApp 已经知道当前用户是谁了。

要解决此问题,IHostEnvironmentAuthenticationStateProvider请先注册一个服务:

services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
services.AddScoped<IHostEnvironmentAuthenticationStateProvider>(sp => {
    // this is safe because 
    //     the `RevalidatingIdentityAuthenticationStateProvider` extends the `ServerAuthenticationStateProvider`
    var provider = (ServerAuthenticationStateProvider) sp.GetRequiredService<AuthenticationStateProvider>();
    return provider;
});
Run Code Online (Sandbox Code Playgroud)

然后创建一个主体并替换旧的。

@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IHostEnvironmentAuthenticationStateProvider HostAuthentication
...

var user = await userManager.FindByNameAsync(UserName);
var valid= await signInManager.UserManager.CheckPasswordAsync(user, Password);

如果(有效)
{
    var principal = await signInManager.CreateUserPrincipalAsync(user);

    var identity = new ClaimsIdentity(
        校长索赔,
        Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme
    );
    主体 = 新 System.Security.Claims.ClaimsPrincipal(identity);
    signInManager.Context.User = 主体;
    HostAuthentication.SetAuthenticationState(Task.FromResult(new AuthenticationState(principal)));

    // 现在更新了 authState
    var authState = 等待 AuthenticationStateProvider.GetAuthenticationStateAsync();

    successMessage = $"{用户名},已登录。";
    错误信息 = "";

}
别的
{
    成功消息 = "";
    errorMessage = "用户名或密码不正确。";
}

演示

在此处输入图片说明

并检查authState

在此处输入图片说明

  • @user1206480 抱歉,我忘记触发 `NotifyAuthenticationStateChanged` 事件。我编辑了我的答案以确保 authState 已更新。请检查 :) (2认同)
  • 它在这里说 https://docs.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-3.1 Razor 组件不支持 SignInManger 和 UserManager 。这是一个可以接受的答案吗?此外,命名空间中不存在 RevalidatingIdentityAuthenticationStateProvider&lt;IdentityUser&gt;,并且会生成编译错误。我只是不明白为什么没有关于此的文档并且“解决方案”如此有问题。 (2认同)
  • RevalidatingIdentityAuthenticationStateProvider&lt;TUser&gt; 在 .NET Core 3.1 中不存在,但 RevalidatingServerAuthenticationStateProvider 确实存在。尝试使用它,我在启动时收到此错误:“无法实例化服务类型“Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider”的实现类型“Microsoft.AspNetCore.Components.Server.RevalidatingServerAuthenticationStateProvider”。” (2认同)