繁体   English   中英

Blazor 服务器端应用程序无法登录

[英]SignIn for Blazor Server-Side app not working

我正在为 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;" }
        };

}

Razor 组件:

@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; }

}

Basically, it happens because the SigninManger::SignInAsync() will actually try to send a cookie over HTTP to indicate this user has already signed in. But when dealing with Blazor Server Side at this moment, there's no available HTTP Response at all, there's只有WebSocket 连接(SignalR)。

怎么修

简而言之,登录就是持久化用户凭据/cookies/...以便 WebApp 知道客户端是谁。 由于您使用的是 Blazor 服务器端,因此您的客户端正在WebSocket 连接中与服务器通信。 无需通过 HTTP 发送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;
});

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

@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IHostEnvironmentAuthenticationStateProvider HostAuthentication
...

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

if (valid)
{
    var principal = await signInManager.CreateUserPrincipalAsync(user);

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

    // now the authState is updated
    var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();

    successMessage = $"{UserName}, signed in.";
    errorMessage = "";

}
else
{
    successMessage = "";
    errorMessage = "Username or password is incorrect.";
}

演示

在此处输入图像描述

并检查authState

在此处输入图像描述

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

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

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

确保您 DI IOptionsMonitor<CookieAuthenticationOptions> ,允许您使用 Scheme 作为键来解析命名选项。 确保您使用.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);
    }

然后我们编写一个 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=/";
        }
    }

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

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

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "MyScheme";
        }).AddCookie("MyScheme", options =>
        {
            options.Cookie.Name = "CookieName";
        });

对于completionist,你也可以用同样的方式实现log off:

    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;
    }

和 JS:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM