注意
此版本不是本文的最新版本。 对于当前版本,请参阅本文的 .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>。 在以下示例中,ApplicationUser
为 IdentityUser。 可以进一步自定义消息 HTML 标记。 只要传递给 MandrillMessage
的 message
以 <
字符开头,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)上的其他电子邮件别名。
- 尝试发送到不同的电子邮件帐户。