使用.NET Blazor实现登录,无需使用Identity脚手架。

4

我正在尝试为我的Blazor-server应用程序添加身份验证/登录功能。

我不想使用设置页面和数据库的Identity功能。

当我在Razor pages中执行此操作时,我可以使用以下代码创建一个登录页面:

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

这将使用cookies进行登录,我可以基于身份验证或是否已登录来阻止页面访问或显示/隐藏内容。

现在,如果我使用Identity,它会为登录和用户管理设置剃刀页面,但我正在尝试从剃刀组件而不是剃刀页面中实现登录功能,而我无法找到一种方法。

我可以尝试注入HttpContextAccessor并使用它:

    HttpContext httpContext = HttpContextAccessor.HttpContext;
    await httpContext.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme, principal);

但是它会抛出一个异常: 未处理的组件渲染异常:Headers are read-only, response has already started. System.InvalidOperationException: Headers are read-only, response has already started.
大多数我能找到的似乎都是使用Razor页面的方法。
难道不能使用Razor组件登录吗?
2个回答

4
可以做到。这是原理:
1.创建一个 Login.razor 组件并注入 SignInManager 和 NavigationManager 。使用 SignInManager 使用 CheckPasswordSignInAsync() 方法来验证密码。不要调用 PasswordSignInAsync() ,因为它会抛出前面提到的异常。相反,将凭据传递给自定义中间件中的凭据缓存(参见下一段)。然后调用 NavigationManager.NavigateTo(/login?key=, true) 执行完整的 postback,这是必需的以设置 cookie。
2.创建一个 Middleware 类(我称其为 BlazorCookieLoginMiddleware ):在其中使用静态字典来缓存来自 Blazor 登录组件的登录信息。此外,拦截 "/login?key=" 请求,然后使用 SignInManager 执行实际的登录。这是有效的,因为当 cookie 可以被设置时,中间件在管道中较早执行。可以从静态字典缓存中检索凭据,并应立即从字典中删除它们。如果身份验证成功,只需将用户重定向到应用程序根目录“/”或任何其他位置即可。
我测试过了,它可以很好地工作。我还成功添加了双因素认证,但那对于本文来说太多了。
以下是一些代码(请注意:为简单起见,未正确处理极端情况和错误):
Login.razor:
@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";
        }
    }
}

BlazorCookieLoginMiddleware.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);
            }
        }
    }

不要忘记将新的中间件添加到 Startup.cs 中:

        public void Configure(IApplicationBuilder app)
        {
            //.....
            app.UseAuthentication();
            app.UseAuthorization();
            
            app.UseMiddleware<BlazorCookieLoginMiddleware>();
            //.....
        }

2
在Blazor组件中无法获取HttpContext,可以参考这个帖子:HttpContext is always null
如果你想在客户端访问声明,应该使用OAuth2或OIDC。例如,你可以使用Authfix/Blazor-Oidcsotsera/sotsera.blazor.oidc
另一种方法是使用经典的Identity Razor页面登录用户,但不在Blazor页面中进行。然后,在Web API的服务器端访问用户声明。但这意味着所有内容都部署在同一主机上。

你可以通过注入来获取HttpContext将 services.AddScoped<HttpClient>(); 添加到你的服务中。现在,你可以直接通过注入来获取HttpClient [Inject] HttpClient _httpClient { get; set; }我也在我的项目中使用了这个方法。你可能需要在nuget中添加 System.Net.Http。我不太确定这一步是否必要,因为那是很久以前的事情了。 - Taladan
但是你的意思是针对客户端Blazor吗?我怀疑在服务器端Blazor上做不到这一点。在代码中设置一个在服务器上执行的cookie似乎几乎不可能。 - Heinzlmaen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接