我正在尝试向我的Blazor服务器应用添加身份验证/登录。 我不想使用身份信息,它可以设置页面和数据库。
当我使用剃须刀页面执行此操作时,我可以有一个用于使用以下代码登录的页面:
var claims = new List<Claim>{
new Claim(ClaimTypes.Name, Input.Name),
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme, principal);
这将使用Cookie登录,并且我可以根据声明或您是否登录来阻止访问页面,或显示/隐藏内容。
现在,如果我使用Identity,它将为登录和用户管理设置剃须刀页面,但是我正尝试从剃须刀组件执行登录功能,而我真的找不到解决方法。 我可以尝试注入HttpContextAccessor并使用它:
HttpContext httpContext = HttpContextAccessor.HttpContext;
await httpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme, principal);
但是它抛出一个异常: 未处理的异常呈现组件:标头是只读的,响应已经开始。 System.InvalidOperationException:标头是只读的,响应已经开始。
我能找到的大多数东西似乎都是使用剃刀页面方法。
是否可以使用剃须刀组件登录?
答案 0 :(得分:2)
无法在blazor组件中获得HttpContext
,请参见以下线程:HttpContext is always null
如果要在客户端访问声明,则应使用 OAuth2 或 OIDC 。您可以使用Authfix/Blazor-Oidc或sotsera/sotsera.blazor.oidc为例
另一种方法是使用身份剃刀页面分类登录用户,但不在您的“ blazor”页面登录。然后在您的Web API中访问服务器端的用户声明。但这意味着所有部署在同一主机上。
答案 1 :(得分:1)
可以做到。这是原理:
创建一个Login.razor组件,并注入SignInManager和NavigationManager。使用SignInManager通过CheckPasswordSignInAsync()方法验证密码。不要调用PasswordSignInAsync(),因为它会引发前面提到的异常。而是将凭据传递到自定义中间件中的凭据缓存(请参阅下一段)。然后调用NavigationManager.NagigateTo(/ login?key =, true )执行完整的回发,这是设置cookie所必需的。
创建一个中间件类(我称为BlazorCookieLoginMiddleware):在其中,您可以使用静态字典来缓存Blazor登录组件中的登录信息。另外,您拦截对“ / login?key =“的请求,然后使用SignInManager执行实际登录。之所以可行,是因为中间件是在管道中较早执行的,此时仍可以设置cookie。可以从静态字典缓存中检索凭证,并应立即将其从dict中删除。如果验证成功,则只需将用户重定向到应用程序根目录“ /”或您想要的任何位置。
我对此进行了测试,它就像一种魅力。我还成功添加了2FA,但这对于这篇文章来说太过分了。
以下是一些代码(请注意:为简单起见,边缘情况和错误未正确处理;只是PoC):
Login.razor:
class VideoPost(models.Model):
....
def save(self, *args, **kw):
old = type(self).objects.get(pk=self.pk) if self.pk else None
super(VideoPost, self).save(*args, **kw)
if not old:
# Do something when creating a new one
if old and old.some_video_field != self.some_video_field: # Field has changed
# Do something to convert video for the video field is updated
BlazorCookieLoginMiddleware.cs:
@page "/login"
@attribute [AllowAnonymous]
@inject SignInManager<ApplicationUser> SignInMgr
@inject UserManager<ApplicationUser> UserMgr
@inject NavigationManager NavMgr
<h3>Login</h3>
<label for="email">Email:</label>
<input type="email" @bind="Email" name="email" />
<label for="password">Password:</label>
<input type="password" @bind="password" name="password" />
@if (!string.IsNullOrEmpty(error))
{
<div class="alert-danger">
<p>@error</p>
</div>
}
<button @onclick="LoginClicked">Login</button>
@code {
public string Email { get; set; }
private string password;
private string error;
private async Task LoginClicked()
{
error = null;
var usr = await UserMgr.FindByEmailAsync(Email);
if (usr == null)
{
error = "User not found";
return;
}
if (await SignInMgr.CanSignInAsync(usr))
{
var result = await SignInMgr.CheckPasswordSignInAsync(usr, password, true);
if (result == Microsoft.AspNetCore.Identity.SignInResult.Success)
{
Guid key = Guid.NewGuid();
BlazorCookieLoginMiddleware.Logins[key] = new LoginInfo { Email = Email, Password = password };
NavMgr.NavigateTo($"/login?key={key}", true);
}
else
{
error = "Login failed. Check your password.";
}
}
else
{
error = "Your account is blocked";
}
}
}
不要忘记将新的中间件添加到 Startup.cs:
public class LoginInfo
{
public string Email { get; set; }
public string Password { get; set; }
}
public class BlazorCookieLoginMiddleware
{
public static IDictionary<Guid, LoginInfo> Logins { get; private set; }
= new ConcurrentDictionary<Guid, LoginInfo>();
private readonly RequestDelegate _next;
public BlazorCookieLoginMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context, SignInManager<ApplicationUser> signInMgr)
{
if (context.Request.Path == "/login" && context.Request.Query.ContainsKey("key"))
{
var key = Guid.Parse(context.Request.Query["key"]);
var info = Logins[key];
var result = await signInMgr.PasswordSignInAsync(info.Email, info.Password, false, lockoutOnFailure: true);
info.Password = null;
if (result.Succeeded)
{
Logins.Remove(key);
context.Response.Redirect("/");
return;
}
else if (result.RequiresTwoFactor)
{
//TODO: redirect to 2FA razor component
context.Response.Redirect("/loginwith2fa/" + key);
return;
}
else
{
//TODO: Proper error handling
context.Response.Redirect("/loginfailed");
return;
}
}
else
{
await _next.Invoke(context);
}
}
}