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

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

命名空间

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

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

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

创建一个类以获取安全电子邮件 API 密钥。 本文中的示例将一个名为 AuthMessageSenderOptions 的类与一个 EmailAuthKey 属性配合使用,以便保存密钥。

AuthMessageSenderOptions

namespace BlazorSample;

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

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

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

为提供程序的安全密钥配置用户机密

使用机密管理器工具设置密钥。 在下面的示例中,密钥名称为 EmailAuthKey,并且密钥由 {KEY} 占位符表示。 在命令行界面中,导航到应用的根文件夹,并使用 API 密钥执行以下命令:

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

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

实现 IEmailSender

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

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(ApplicationUser user, string email, 
        string confirmationLink) => SendEmailAsync(email, "Confirm your email", 
        $"Please confirm your account by " +
        "<a href='{confirmationLink}'>clicking here</a>.");

    public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, 
        string resetLink) => SendEmailAsync(email, "Reset your password", 
        $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");

    public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, 
        string resetCode) => SendEmailAsync(email, "Reset your password", 
        $"Please reset your password using the following code: {resetCode}");

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

更改所有数据保护令牌的使用期限

以下代码将所有数据保护令牌超时期限更改为 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)上的其他电子邮件别名。
  • 尝试发送到不同的电子邮件帐户。

警告

请勿在测试和开发中使用生产机密。 如果将应用发布到 Azure,请在 Azure Web 应用门户中将机密设为应用程序设置。 配置系统设置为从环境变量读取密钥。

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

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

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

其他资源