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

时间:2019-10-15 05:19:20

标签: c# blazor-server-side asp.net-core-3.0

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

剃刀组件页面:

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

}

剃刀组件:

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

}

2 个答案:

答案 0 :(得分:2)

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

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

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

确保您对 IOptionsMonitor<CookieAuthenticationOptions> 进行 DI,允许您使用 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);
    }

然后我们write a 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";
        });

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

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

答案 1 :(得分:1)

基本上,是因为SigninManger::SignInAsync()实际上会尝试在HTTP上通过发送cookie 来指示该用户已经登录。但是在与Blazor Server Side进行交易时目前,完全没有 HTTP响应,只有 WebSocket连接(SignalR)。

如何修复

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

演示

enter image description here

并检查authState

enter image description here