无需身份验证脚架的.NET Blazor登录

时间:2019-11-19 18:20:17

标签: .net authentication blazor blazor-server-side

我正在尝试向我的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:标头是只读的,响应已经开始。

我能找到的大多数东西似乎都是使用剃刀页面方法。

是否可以使用剃须刀组件登录?

2 个答案:

答案 0 :(得分:2)

无法在blazor组件中获得HttpContext,请参见以下线程:HttpContext is always null

如果要在客户端访问声明,则应使用 OAuth2 OIDC 。您可以使用Authfix/Blazor-Oidcsotsera/sotsera.blazor.oidc为例

另一种方法是使用身份剃刀页面分类登录用户,但不在您的“ blazor”页面登录。然后在您的Web API中访问服务器端的用户声明。但这意味着所有部署在同一主机上。

答案 1 :(得分:1)

可以做到。这是原理:

  1. 创建一个Login.razor组件,并注入SignInManager和NavigationManager。使用SignInManager通过CheckPasswordSignInAsync()方法验证密码。不要调用PasswordSignInAsync(),因为它会引发前面提到的异常。而是将凭据传递到自定义中间件中的凭据缓存(请参阅下一段)。然后调用NavigationManager.NagigateTo(/ login?key =, true )执行完整的回发,这是设置cookie所必需的。

  2. 创建一个中间件类(我称为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);
            }
        }
    }
相关问题