我正在为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; }
}
答案 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)。
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."; }
并检查authState
: