ASP.NET Core Blazor 中的帐户确认和密码恢复

注意

此版本不是本文的最新版本。 对于当前版本,请参阅本文的 .NET 9 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅本文的 .NET 9 版本

本文介绍如何为 ASP.NET Core Blazor Web App 配置电子邮件确认和密码恢复。

注意

本文仅适用于 Blazor Web App。 若要为具有 ASP.NET Core Identity 的独立 Blazor WebAssembly 应用实现电子邮件确认和密码恢复,请参阅具有 ASP.NET Core Identity 的 ASP.NET Core Blazor WebAssembly 中的帐户确认和密码恢复

Namespace

本文中示例使用的应用命名空间为 BlazorSample。 更新这些代码示例以使用你的应用的命名空间。

选择并配置电子邮件提供程序

本文中使用 Mailchimp 的事务 API 通过 Mandrill.net 发送电子邮件。 建议使用电子邮件服务(而不是 SMTP)来发送电子邮件。 SMTP 难以正确配置和保护。 无论使用哪种电子邮件服务,都请访问其 .NET 应用指南、创建帐户、为其服务配置 API 密钥,并安装所需的任何 NuGet 包。

创建用于保存机密电子邮件提供程序 API 密钥的类。 本文中的示例结合使用一个名为 AuthMessageSenderOptions 属性的类和一个 EmailAuthKey 属性来保存密钥。

AuthMessageSenderOptions.cs:

namespace BlazorSample;

public class AuthMessageSenderOptions
{
    public string? EmailAuthKey { get; set; }
}

Program 文件中注册 AuthMessageSenderOptions 配置实例:

builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

为电子邮件提供程序的安全密钥配置机密

从提供商接收电子邮件提供商的安全密钥,并在以下指南中使用它。

使用以下任一方法或两种方法向应用提供机密:

  • 机密管理器工具:机密管理器工具将专用数据存储在本地计算机上,仅在本地开发期间使用。
  • Azure Key Vault:可以在密钥保管库中存储机密,以便在任何环境中使用,包括在本地工作时用于开发环境。 一些开发人员更喜欢使用密钥保管库进行过渡和生产部署,并使用 机密管理器工具 进行本地开发。

强烈建议避免将机密存储在项目代码或配置文件中。 使用安全身份验证流,例如本部分中的任一或两种方法。

机密管理器工具

如果已为机密管理器工具初始化了项目,则其项目文件 (.csproj) 中将已有应用机密标识符 (<AppSecretsId>)。 在 Visual Studio 中,要判断应用机密 ID 是否存在,只需在解决方案资源管理器中选择了项目后查看“属性”面板。 如果应用尚未初始化,请在打开至项目的目录的命令 shell 中执行以下命令。 在 Visual Studio 中,你可以使用开发人员 PowerShell 命令提示。

dotnet user-secrets init

使用机密管理器工具设置 API 密钥。 在下面的示例中,密钥名称是与 AuthMessageSenderOptions.EmailAuthKey 匹配的 EmailAuthKey,密钥由 {KEY} 占位符表示。 使用 API 密钥执行以下命令:

dotnet user-secrets set "EmailAuthKey" "{KEY}"

如果使用 Visual Studio,可以通过右键单击“解决方案资源管理器”中的服务器项目并选择“管理用户机密”来确认机密是否已设置。

有关详细信息,请参阅在 ASP.NET Core 开发中安全存储应用机密

警告

不要在客户端代码中存储应用机密、连接字符串、凭据、密码、个人标识号 (PIN)、专用 C#/.NET 代码或私钥/令牌,这始终不安全。 在测试/暂存和生产环境中,服务器端 Blazor 代码和 Web API 应使用安全身份验证流,以避免在项目代码或配置文件中维护凭据。 在本地开发测试之外,我们建议避免使用环境变量来存储敏感数据,因为环境变量不是最安全的方法。 对于本地开发测试,建议使用机密管理器工具来保护敏感数据。 有关详细信息,请参阅安全维护敏感数据和凭据

Azure Key Vault

Azure Key Vault 提供了一种安全的方法,用于向应用提供应用的客户端密码。

若要创建密钥保管库并设置机密,请参阅 关于 Azure Key Vault 机密(Azure 文档),其中跨链接资源以开始使用 Azure Key Vault。 对于本部分中的示例,机密名称为“EmailAuthKey.”

在 Entra 或 Azure 门户中建立密钥保管库时:

  • 将密钥保管库配置为使用 Azure 基于角色的访问控制(RABC)。 如果您未在 Azure 虚拟网络上运行(包括用于本地开发和测试),请确认在网络步骤中,公共访问已启用(已选中)。 启用公共访问仅会公开密钥保管库终结点。 访问仍需要经过身份验证的帐户。

  • 使用“密钥保管库机密用户”角色创建 Azure 托管 Identity(或向计划使用的现有托管 Identity 添加角色)。 将托管Identity分配给托管部署的 Azure 应用服务:设置>Identity>用户分配>添加

    注意

    如果还计划使用 Azure CLI 或 Visual Studio 的 Azure 服务身份验证通过授权用户在本地运行应用,请使用 Key Vault 机密用户角色在访问控制(IAM)中添加开发人员 Azure 用户帐户。 若要通过 Visual Studio 使用 Azure CLI,请从开发人员 PowerShell 面板执行 az login 该命令,并按照提示向租户进行身份验证。

若要实现本节中的代码,在创建密钥保管库和机密时,请记录密钥保管库的 URI(例如:“https://contoso.vault.azure.net/”,需要尾部反斜杠)和机密名称(例如:“EmailAuthKey”)。

重要

密钥保管库机密是在设置了过期日期的情况下创建的。 请务必跟踪密钥保管库机密何时过期,并在该日期之前为应用创建新的机密。

将以下 AzureHelper 类添加到服务器项目。 GetKeyVaultSecret 方法从密钥保管库中检索机密。 调整命名空间(BlazorSample.Helpers)以匹配项目命名空间方案。

Helpers/AzureHelper.cs:

using Azure.Core;
using Azure.Security.KeyVault.Secrets;

namespace BlazorSample.Helpers;

public static class AzureHelper
{
    public static string GetKeyVaultSecret(string vaultUri, 
        TokenCredential credential, string secretName)
    {
        var client = new SecretClient(new Uri(vaultUri), credential);
        var secret = client.GetSecretAsync(secretName).Result;

        return secret.Value.Value;
    }
}

如果在服务器项目的 Program 文件中注册服务,请使用选项配置获取并绑定机密:

TokenCredential? credential;

if (builder.Environment.IsProduction())
{
    credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
    // Local development and testing only
    DefaultAzureCredentialOptions options = new()
    {
        // Specify the tenant ID to use the dev credentials when running the app locally
        // in Visual Studio.
        VisualStudioTenantId = "{TENANT ID}",
        SharedTokenCacheTenantId = "{TENANT ID}"
    };

    credential = new DefaultAzureCredential(options);
}

var emailAuthKey = AzureHelper.GetKeyVaultSecret("{VAULT URI}", credential, 
    "EmailAuthKey");

var authMessageSenderOptions = 
    new AuthMessageSenderOptions() { EmailAuthKey = emailAuthKey };
builder.Configuration.GetSection(authMessageSenderOptions.EmailAuthKey)
    .Bind(authMessageSenderOptions);

注意

在非生产环境中,前面的示例用于 DefaultAzureCredential 简化身份验证,同时开发部署到 Azure 的应用,方法是将 Azure 托管环境中使用的凭据与本地开发中使用的凭据组合在一起。 有关详细信息,请参阅 使用系统分配的托管标识向 Azure 资源验证 Azure 托管的 .NET 应用

前面的示例意味着托管 Identity 客户端 ID ({MANAGED IDENTITY CLIENT ID})、目录(租户)ID ({TENANT ID}) 以及密钥保管库 URI({VAULT URI},例如:https://contoso.vault.azure.net/,需要尾部反斜杠)是由硬编码值提供的。 可以从应用设置配置提供所有这些值或全部值。 例如,以下代码从应用程序设置文件的 AzureAd 节点获取保管库 URI,vaultUri 可以用于前面示例中对 GetKeyVaultSecret 的调用:

var vaultUri = builder.Configuration.GetValue<string>("AzureAd:VaultUri")!;

有关详细信息,请参阅 ASP.NET Core Blazor 配置

实现 IEmailSender

以下示例基于 Mailchimp 的事务 API,该 API 使用 Mandrill.net。 对于其他提供程序,请参阅有关如何实现发送电子邮件的文档。

Mandrill.net NuGet 包添加到项目。

添加以下 EmailSender 类以实现 IEmailSender<TUser>。 在以下示例中,ApplicationUserIdentityUser。 可以进一步自定义消息 HTML 标记。 只要传递给 MandrillMessagemessage< 字符开头,Mandrill.net API 就假定消息正文以 HTML 形式编写。

Components/Account/EmailSender.cs:

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Mandrill;
using Mandrill.Model;
using BlazorSample.Data;

namespace BlazorSample.Components.Account;

public class EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor,
    ILogger<EmailSender> logger) : IEmailSender<ApplicationUser>
{
    private readonly ILogger logger = logger;

    public AuthMessageSenderOptions Options { get; } = optionsAccessor.Value;

    public Task SendConfirmationLinkAsync(AppUser user, string email,
        string confirmationLink) => SendEmailAsync(email, "Confirm your email",
        "<html lang=\"en\"><head></head><body>Please confirm your account by " +
        $"<a href='{confirmationLink}'>clicking here</a>.</body></html>");

    public Task SendPasswordResetLinkAsync(AppUser user, string email,
        string resetLink) => SendEmailAsync(email, "Reset your password",
        "<html lang=\"en\"><head></head><body>Please reset your password by " +
        $"<a href='{resetLink}'>clicking here</a>.</body></html>");

    public Task SendPasswordResetCodeAsync(AppUser user, string email,
        string resetCode) => SendEmailAsync(email, "Reset your password",
        "<html lang=\"en\"><head></head><body>Please reset your password " +
        $"using the following code:<br>{resetCode}</body></html>");

    public async Task SendEmailAsync(string toEmail, string subject, string message)
    {
        if (string.IsNullOrEmpty(Options.EmailAuthKey))
        {
            throw new Exception("Null EmailAuthKey");
        }

        await Execute(Options.EmailAuthKey, subject, message, toEmail);
    }

    public async Task Execute(string apiKey, string subject, string message, 
        string toEmail)
    {
        var api = new MandrillApi(apiKey);
        var mandrillMessage = new MandrillMessage("sarah@contoso.com", toEmail, 
            subject, message);
        await api.Messages.SendAsync(mandrillMessage);

        logger.LogInformation("Email to {EmailAddress} sent!", toEmail);
    }
}

注意

邮件的正文内容可能需要电子邮件服务提供程序的特殊编码。 如果电子邮件中无法跟踪消息正文中的链接,请参阅服务提供商的文档来解决问题。

将应用配置为支持电子邮件

Program 文件中,将电子邮件发件人实现更改为 EmailSender

- builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
+ builder.Services.AddSingleton<IEmailSender<ApplicationUser>, EmailSender>();

从应用中删除 IdentityNoOpEmailSender (Components/Account/IdentityNoOpEmailSender.cs)。

RegisterConfirmation 组件 (Components/Account/Pages/RegisterConfirmation.razor) 中,删除 @code 块中用于检查 EmailSender 是否是 IdentityNoOpEmailSender 的条件块:

- else if (EmailSender is IdentityNoOpEmailSender)
- {
-     ...
- }

此外,在 RegisterConfirmation 组件中,删除用于检查 emailConfirmationLink 字段的 Razor 标记和代码,只留下指示用户检查其电子邮件的行。

- @if (emailConfirmationLink is not null)
- {
-     ...
- }
- else
- {
     <p>Please check your email to confirm your account.</p>
- }

@code {
-    private string? emailConfirmationLink;

     ...
}

在站点具有用户后启用帐户确认

在具有用户的站点上启用帐户确认会锁定所有现有用户。 现有用户被锁定,因为未确认其帐户。 若要解决现有用户锁定问题,请使用以下方法之一:

  • 更新数据库以将所有现有用户标记为已经过确认。
  • 确认现有用户。 例如,批量发送包含确认链接的电子邮件。

电子邮件和活动超时

默认的非活动超时为 14 天。 下面的代码将非活动超时设置为 5 天,并启用可调过期:

builder.Services.ConfigureApplicationCookie(options => {
    options.ExpireTimeSpan = TimeSpan.FromDays(5);
    options.SlidingExpiration = true;
});

更改所有 ASP.NET Core 数据保护令牌的使用期限

以下代码将数据保护令牌超时期限更改为 3 小时:

builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
    options.TokenLifespan = TimeSpan.FromHours(3));

内置 Identity 用户令牌 (AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs) 的超时为 1 天

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

更改电子邮件令牌的使用期限

Identity 用户令牌的默认令牌有效期是 1 天

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

若要更改电子邮件令牌有效期,请添加自定义的 DataProtectorTokenProvider<TUser>DataProtectionTokenProviderOptions

CustomTokenProvider.cs:

using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;

namespace BlazorSample;

public class CustomEmailConfirmationTokenProvider<TUser>
    : DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomEmailConfirmationTokenProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<EmailConfirmationTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
        : base(dataProtectionProvider, options, logger)
    {
    }
}

public class EmailConfirmationTokenProviderOptions 
    : DataProtectionTokenProviderOptions
{
    public EmailConfirmationTokenProviderOptions()
    {
        Name = "EmailDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(4);
    }
}

public class CustomPasswordResetTokenProvider<TUser> 
    : DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomPasswordResetTokenProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<PasswordResetTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
        : base(dataProtectionProvider, options, logger)
    {
    }
}

public class PasswordResetTokenProviderOptions : 
    DataProtectionTokenProviderOptions
{
    public PasswordResetTokenProviderOptions()
    {
        Name = "PasswordResetDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(3);
    }
}

Program 文件中将服务配置为使用自定义令牌提供程序:

builder.Services.AddIdentityCore<ApplicationUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
        options.Tokens.ProviderMap.Add("CustomEmailConfirmation",
            new TokenProviderDescriptor(
                typeof(CustomEmailConfirmationTokenProvider<ApplicationUser>)));
        options.Tokens.EmailConfirmationTokenProvider = 
            "CustomEmailConfirmation";
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

builder.Services
    .AddTransient<CustomEmailConfirmationTokenProvider<ApplicationUser>>();

故障排除

如果无法使用电子邮件:

  • EmailSender.Execute 中设置断点,以验证是否调用 SendEmailAsync
  • 使用类似于 EmailSender.Execute 的代码创建控制台应用来发送电子邮件,从而调试问题。
  • 查看电子邮件提供程序网站上的帐户电子邮件历史记录页。
  • 检查垃圾邮件文件夹中是否有邮件。
  • 尝试使用其他电子邮件提供程序(例如 Microsoft、Yahoo 或 Gmail)上的其他电子邮件别名。
  • 尝试发送到不同的电子邮件帐户。

其他资源