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

注意

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

重要

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

有关当前版本,请参阅本文.NET 9 版本。

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

注意

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

命名空间

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

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

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

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

AuthMessageSenderOptions.cs:

namespace BlazorSample;

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

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

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

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

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

dotnet user-secrets init

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

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

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

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

警告

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

实现 IEmailSender

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

Mandrill.net NuGet 包添加到项目。

添加以下 EmailSender 类以实现 IEmailSender<TUser>。 在下面的示例中, ApplicationUser 是一个 IdentityUser。 可以进一步自定义消息 HTML 标记。 只要 message 传递以 MandrillMessage 字符开头 < ,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)超时一

注意

指向 .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)上的其他电子邮件别名。
  • 尝试发送到不同的电子邮件帐户。

其他资源