在 Blazor 模板帐户页面上启用交互式渲染模式

问题描述 投票:0回答:1

问题:

我正在开发一个 Blazor 项目,其中所有帐户页面的模板设置为

RenderMode.Server
。我想让其中一些帐户页面具有交互性,因此我尝试启用交互式渲染模式。

但是,我在这样做时遇到了多个问题:

  1. 无限循环:如果我通过删除帐户页面上的过滤器来全局启用交互式渲染模式,我会遇到无限循环。
  2. Null 异常:在特定页面上启用交互模式时,遇到空异常。
  3. 响应已开始错误:通过检查绕过空异常后,出现以下错误:
    System.InvalidOperationException: OnStarting cannot be set because the response has already started.
       at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value)
    

在 Blazor 中为帐户页面启用交互式呈现模式而不遇到这些问题的正确方法是什么?如何正确处理身份验证和重定向以避免这些错误?

任何指导或示例将不胜感激!

这是我当前的示例页面(

ExternalLogin.razor
):

@page "/Account/ExternalLogin"

@rendermode @(new InteractiveServerRenderMode(prerender: false))

@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject IUserStore<ApplicationUser> UserStore
@inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
@inject ILogger<ExternalLogin> Logger

<PageTitle>Register</PageTitle>

<StatusMessage Message="@message" />
<h1>Register</h1>
<h2>Associate your @ProviderDisplayName account.</h2>
<hr />

<div class="alert alert-info">
    You've successfully authenticated with <strong>@ProviderDisplayName</strong>.
    Please enter an email address for this site below and select the account type, then click the Register button to finish logging in.
</div>

<div class="row">
    <div class="col-md-4">
        <RadzenTemplateForm TItem="InputModel" Data=@Input Submit=@OnValidSubmitAsync>
            <RadzenFieldset Text="Register">
                <div class="row mb-5">
                    <div class="col-md-4" style="align-self: center;">
                        <RadzenLabel Text="Email" Component="Email" />
                    </div>
                    <div class="col">
                        <RadzenTextBox style="display: block" Name="Input.Email" @[email protected] class="w-100" Placeholder="Please enter your email" />
                        <RadzenRequiredValidator Component="Input.Email" Text="Email is required" Popup="true" Style="position: absolute" />
                        <RadzenEmailValidator Component="Input.Email" Text="Provide a valid email address" Popup="true" Style="position: absolute" />
                    </div>
                </div>
                <div class="row mb-5">
                    <div class="col-md-4" style="align-self: center;">
                        <RadzenLabel Text="Account Type" Component="AccountType" />
                    </div>
                    <div class="col">
                        <RadzenSelectBar @[email protected] TValue="string" class="mb-5">
                            <Items>
                                <RadzenSelectBarItem Icon="person" Text="Customer" Value="RoleConstants.CustomerRoleName" IconColor="Colors.Info" />
                                <RadzenSelectBarItem Icon="cleaning_services" Text="Cleaner" Value="RoleConstants.StaffRoleName" IconColor="@Colors.Success" />
                            </Items>
                        </RadzenSelectBar>
                     </div>
                </div>
            </RadzenFieldset>
            <RadzenButton ButtonType="ButtonType.Submit" Size="ButtonSize.Large" Icon="save" Text="Register" />
        </RadzenTemplateForm>
    </div>
</div>

@code {
    public const string LoginCallbackAction = "LoginCallback";

    private string? message;
    private ExternalLoginInfo externalLoginInfo = default!;

    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    [SupplyParameterFromForm]
    private InputModel Input { get; set; } = new();

    [SupplyParameterFromQuery]
    private string? RemoteError { get; set; }

    [SupplyParameterFromQuery]
    private string? ReturnUrl { get; set; }

    [SupplyParameterFromQuery]
    private string? Action { get; set; }

    private string? ProviderDisplayName => externalLoginInfo?.ProviderDisplayName;

    protected override async Task OnInitializedAsync()
    {
        if (RemoteError is not null)
        {
            RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext);
        }

        var info = await SignInManager.GetExternalLoginInfoAsync();
        if (info is null)
        {
            RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
        }

        externalLoginInfo = info;

        if (HttpMethods.IsGet(HttpContext.Request.Method))
        {
            if (Action == LoginCallbackAction)
            {
                await OnLoginCallbackAsync();
                return;
            }

            // We should only reach this page via the login callback, so redirect back to
            // the login page if we get here some other way.
            RedirectManager.RedirectTo("Account/Login");
        }
    }
    
    protected override void OnParametersSet()
    {
        if (HttpContext is null)
        {
            // If this code runs, we're currently rendering in interactive mode, so there is no HttpContext.
            // The identity pages need to set cookies, so they require an HttpContext. To achieve this we
            // must transition back from interactive mode to a server-rendered page.
            NavigationManager.Refresh(forceReload: true);
        }
    }

    private async Task OnLoginCallbackAsync()
    {
        // Sign in the user with this external login provider if the user already has a login.
        var result = await SignInManager.ExternalLoginSignInAsync(
            externalLoginInfo.LoginProvider,
            externalLoginInfo.ProviderKey,
            isPersistent: false,
            bypassTwoFactor: true);

        if (result.Succeeded)
        {
            Logger.LogInformation(
                "{Name} logged in with {LoginProvider} provider.",
                externalLoginInfo.Principal.Identity?.Name,
                externalLoginInfo.LoginProvider);
            RedirectManager.RedirectTo(ReturnUrl);
        }
        else if (result.IsLockedOut)
        {
            RedirectManager.RedirectTo("Account/Lockout");
        }

        // If the user does not have an account, then ask the user to create an account.
        if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
        {
            Input.Email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? "";
        }
    }

    private async Task OnValidSubmitAsync()
    {
        var emailStore = GetEmailStore();
        var user = CreateUser();

        await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);

        var result = await UserManager.CreateAsync(user);
        if (result.Succeeded)
        {
            result = await UserManager.AddLoginAsync(user, externalLoginInfo);
            if (result.Succeeded)
            {
                Logger.LogInformation("User created an account using {Name} provider.", externalLoginInfo.LoginProvider);

                var userId = await UserManager.GetUserIdAsync(user);
                var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

                var callbackUrl = NavigationManager.GetUriWithQueryParameters(
                    NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
                    new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
                await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));

                // If account confirmation is required, we need to show the link if we don't have a real email sender
                if (UserManager.Options.SignIn.RequireConfirmedAccount)
                {
                    RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email });
                }

                await SignInManager.SignInAsync(user, isPersistent: false, externalLoginInfo.LoginProvider);
                RedirectManager.RedirectTo(ReturnUrl);
            }
        }

        message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}";
    }

    private ApplicationUser CreateUser()
    {
        try
        {
            return Activator.CreateInstance<ApplicationUser>();
        }
        catch
        {
            throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
                $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor");
        }
    }

    private IUserEmailStore<ApplicationUser> GetEmailStore()
    {
        if (!UserManager.SupportsUserEmail)
        {
            throw new NotSupportedException("The default UI requires a user store with email support.");
        }
        return (IUserEmailStore<ApplicationUser>)UserStore;
    }

    private sealed class InputModel
    {
        [Required]
        [EmailAddress]
        public string? Email { get; set; }

        [Required]
        public string AccountType { get; set; } = RoleConstants.CustomerRoleName;
    }
}

问题:

  1. 当我通过删除帐户页面上的过滤器来全局启用交互模式时,我会出现无限循环。
  2. 当我启用它时

在此特定页面上,我收到空异常。添加空检查来绕过这些会导致“响应已开始”错误。

我尝试过的:

  • 全局交互模式:导致无限循环。
  • 特定于页面的交互模式:导致空异常和“响应已开始”错误。

查看其他问题和答案,但他们似乎都通过在帐户页面上禁用它来将其解决为“已修复”,这在我的情况下不是修复,它接受该错误为不可修复。

我所期待的: 该页面具有交互性并且可以像其他人一样工作

asp.net-core authentication blazor rendering
1个回答
0
投票

您不能在帐户页面中使用交互式渲染模式。该模板使用全局过滤器对它们进行 SSR,因为它们需要 httpcontext 才能工作。 (如SignInManager、SigninAsync等)

HttpContext 仅在 SSR 中可用。当处于交互模式时,页面实际上使用的是 websocket 而不是 Http。

表单按钮也适用于 SSR,因此您不必对这些页面使用交互。

© www.soinside.com 2019 - 2024. All rights reserved.