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

作者:Rick AndersonPonantJoe Audette

本教程介绍如何使用电子邮件确认和密码重置构建 ASP.NET Core 应用。 本教程不是开篇引入话题。 你应该熟悉以下内容:

有关 Blazor 指南,请参阅 ASP.NET Core Blazor 中的帐户确认和密码恢复

先决条件

创建和测试使用身份验证的 Web 应用

运行以下命令,创建使用身份验证的 Web 应用。

dotnet new webapp -au Individual -o WebPWrecover
cd WebPWrecover
dotnet run

使用模拟电子邮件确认函注册用户

运行应用,然后选择“注册”链接,注册用户。 注册后,你将重定向到 /Identity/Account/RegisterConfirmation 页,其中包含用于模拟电子邮件确认的链接:

  • 选择 Click here to confirm your account 链接。
  • 选择“登录”链接,并使用相同的凭据登录。
  • 选择 Hello YourEmail@provider.com! 链接,这会重定向到 /Identity/Account/Manage/PersonalData 页面。
  • 选择左侧的“个人数据”选项卡,然后选择“删除”。

显示 Click here to confirm your account 链接是因为尚未使用依赖项注入容器实施并注册 IEmailSender。 请参阅RegisterConfirmation

注意

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

配置电子邮件提供程序

在本教程中,SendGrid 用于发送电子邮件。 需要 SendGrid 帐户和密钥才能发送电子邮件。 建议使用 SendGrid 或其他电子邮件服务(而不是 SMTP)来发送电子邮件。 SMTP 难以确保安全和正确设置。

SendGrid 帐户可能需要添加发件人

创建一个类以获取安全电子邮件密钥。 在此示例中,请创建 Services/AuthMessageSenderOptions.cs

namespace WebPWrecover.Services;

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

配置 SendGrid 用户机密

使用机密管理工具设置 SendGridKey。 例如:

dotnet user-secrets set SendGridKey <key>

Successfully saved SendGridKey to the secret store.

在 Windows 上,机密管理器将密钥/值对存储在 %APPDATA%/Microsoft/UserSecrets/<WebAppName-userSecretsId> 目录中的“secrets.json”文件中。

secrets.json”文件的内容未加密。 以下标记显示“secrets.json”文件。 已删除 SendGridKey 值。

{
  "SendGridKey": "<key removed>"
}

有关详细信息,请参阅选项模式配置

安装 SendGrid

本教程虽然演示的是如何通过 SendGrid 添加电子邮件通知,不过也可以使用其他电子邮件提供程序。

安装 SendGrid NuGet 包:

在包管理器控制台中输入以下命令:

Install-Package SendGrid

请参阅免费开始使用 SendGrid 以注册免费的 SendGrid 帐户。

实现 IEmailSender

使用类似于下面的代码创建 Services/EmailSender.cs 以实施 IEmailSender

using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;

namespace WebPWrecover.Services;

public class EmailSender : IEmailSender
{
    private readonly ILogger _logger;

    public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor,
                       ILogger<EmailSender> logger)
    {
        Options = optionsAccessor.Value;
        _logger = logger;
    }

    public AuthMessageSenderOptions Options { get; } //Set with Secret Manager.

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

    public async Task Execute(string apiKey, string subject, string message, string toEmail)
    {
        var client = new SendGridClient(apiKey);
        var msg = new SendGridMessage()
        {
            From = new EmailAddress("Joe@contoso.com", "Password Recovery"),
            Subject = subject,
            PlainTextContent = message,
            HtmlContent = message
        };
        msg.AddTo(new EmailAddress(toEmail));

        // Disable click tracking.
        // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
        msg.SetClickTracking(false, false);
        var response = await client.SendEmailAsync(msg);
        _logger.LogInformation(response.IsSuccessStatusCode 
                               ? $"Email to {toEmail} queued successfully!"
                               : $"Failure Email to {toEmail}");
    }
}

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

将以下代码添加到 Program.cs 文件:

  • EmailSender 添加为临时服务。
  • 注册 AuthMessageSenderOptions 配置实例。
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using WebPWrecover.Data;
using WebPWrecover.Services;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

当 Account.RegisterConfirmation 已搭建基架时禁用默认帐户验证

此部分仅适用于 Account.RegisterConfirmation 已搭建基架的情况。 如果 Account.RegisterConfirmation 尚未搭建基架,则请跳过此部分。

会将用户重定向到 Account.RegisterConfirmation,用户可以从中选择一个链接来确认帐户。 默认值 Account.RegisterConfirmation 仅用于测试,应在生产应用中禁用自动帐户验证

若要要求使用已确认的帐户,并防止注册时立即登录,请在已搭建基架的“/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs”文件中设置 DisplayConfirmAccountLink = false

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable

using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;

namespace WebPWrecover.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterConfirmationModel : PageModel
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly IEmailSender _sender;

        public RegisterConfirmationModel(UserManager<IdentityUser> userManager, IEmailSender sender)
        {
            _userManager = userManager;
            _sender = sender;
        }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public string Email { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public bool DisplayConfirmAccountLink { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public string EmailConfirmationUrl { get; set; }

        public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
        {
            if (email == null)
            {
                return RedirectToPage("/Index");
            }
            returnUrl = returnUrl ?? Url.Content("~/");

            var user = await _userManager.FindByEmailAsync(email);
            if (user == null)
            {
                return NotFound($"Unable to load user with email '{email}'.");
            }

            Email = email;
            // Once you add a real email sender, you should remove this code that lets you confirm the account
            DisplayConfirmAccountLink = false;
            if (DisplayConfirmAccountLink)
            {
                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                EmailConfirmationUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                    protocol: Request.Scheme);
            }

            return Page();
        }
    }
}

仅当 Account.RegisterConfirmation 已搭建基架时才需要执行此步骤。 未搭建基架的 RegisterConfirmation 会自动检测何时使用依赖项注入容器实施并注册的 IEmailSender

注册、确认电子邮件并重置密码

运行 Web 应用,并测试帐户确认和密码恢复流。

  • 运行应用并注册新用户
  • 查看电子邮件中的帐户确认链接。 如果未收到电子邮件,请参阅调试电子邮件
  • 单击链接以确认你的电子邮件。
  • 使用电子邮件和密码登录。
  • 注销。

测试密码重置

  • 如果已登录,请选择“注销”。
  • 选择“登录”链接,然后选择“忘记了密码?”链接。
  • 输入你注册该帐户时使用的电子邮件。
  • 系统将发送一封包含密码重置链接的电子邮件。 查看你的电子邮件,然后单击链接以重置密码。 密码重置成功后,可使用电子邮件和新密码登录。

重新发送电子邮件确认

选择“登录”页上的“重新发送电子邮件确认”链接。

更改电子邮件和活动超时

默认的非活动超时为 14 天。 下面的代码将非活动超时设置为 5 天:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using WebPWrecover.Data;
using WebPWrecover.Services;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

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

var app = builder.Build();

// Code removed for brevity

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

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

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using WebPWrecover.Data;
using WebPWrecover.Services;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

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

var app = builder.Build();

// Code removed for brevity.

内置 Identity 用户令牌(请参阅 AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs)在 1 天后超时

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

Identity 用户令牌的默认令牌有效期是 1 天。 本部分介绍如何更改电子邮件令牌的使用期限。

添加自定义 DataProtectorTokenProvider<TUser>DataProtectionTokenProviderOptions

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

将自定义提供程序添加到服务容器:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using WebPWrecover.Data;
using WebPWrecover.Services;
using WebPWrecover.TokenProviders;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(config =>
{
    config.SignIn.RequireConfirmedEmail = true;
    config.Tokens.ProviderMap.Add("CustomEmailConfirmation",
        new TokenProviderDescriptor(
            typeof(CustomEmailConfirmationTokenProvider<IdentityUser>)));
    config.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";
}).AddEntityFrameworkStores<ApplicationDbContext>();

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

builder.Services.AddRazorPages();

builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

var app = builder.Build();

// Code removed for brevity.

调试电子邮件

如果无法使用电子邮件:

  • EmailSender.Execute 中设置断点,以验证是否调用 SendGridClient.SendEmailAsync
  • 使用类似于 EmailSender.Execute 的代码创建控制台应用来发送电子邮件
  • 查看电子邮件活动页。
  • 查看垃圾邮件文件夹。
  • 尝试在 Microsoft、Yahoo、Gmail 等不同的电子邮件提供程序上使用另一个电子邮件别名
  • 尝试发送到不同的电子邮件帐户。

安全最佳做法是在测试和开发中避免使用生产机密。 如果将应用发布到 Azure,请在 Azure Web 应用门户中将“SendGrid 机密”设置为“应用程序设置”。 配置系统设置为从环境变量读取密钥。

合并社交和本地登录帐户

若要完成本部分,必须首先启用外部身份验证提供程序。 请参阅 Facebook、Google 和外部提供程序身份验证

可通过单击电子邮件链接来合并本地帐户和社交帐户。 在下面的序列中,“RickAndMSFT@gmail.com”首先创建为本地登录;但也可以先将帐户创建为社交登录,然后添加本地登录。

Web application: RickAndMSFT@gmail.com user authenticated

单击“管理”链接。 请注意,与此帐户关联的外部登录(社交登录)为 0。

Manage view

单击另一个登录服务链接并接受应用请求。 下图中,Facebook 是外部身份验证提供程序:

Manage your external logins view listing Facebook

这两个帐户已合并。 可使用任一帐户登录。 你可能希望用户添加本地帐户,以防其社交登录身份验证服务关闭,或者更有可能失去其社交帐户的访问权限。

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

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

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

先决条件

.NET Core 3.0 SDK 或更高版本

创建和测试使用身份验证的 Web 应用

运行以下命令,创建使用身份验证的 Web 应用。

dotnet new webapp -au Individual -uld -o WebPWrecover
cd WebPWrecover
dotnet run

运行应用,然后选择“注册”链接,注册用户。 注册后,你将重定向到 /Identity/Account/RegisterConfirmation 页,其中包含用于模拟电子邮件确认的链接:

  • 选择 Click here to confirm your account 链接。
  • 选择“登录”链接,并使用相同的凭据登录。
  • 选择 Hello YourEmail@provider.com! 链接,这会将你重定向到 /Identity/Account/Manage/PersonalData 页面。
  • 选择左侧的“个人数据”选项卡,然后选择“删除”。

配置电子邮件提供程序

在本教程中,SendGrid 用于发送电子邮件。 可使用其他电子邮件提供程序。 建议使用 SendGrid 或其他电子邮件服务发送电子邮件。 SMTP 很难配置,因此不会将邮件标记为垃圾邮件。

SendGrid 帐户可能需要添加发件人

创建一个类以获取安全电子邮件密钥。 在此示例中,请创建 Services/AuthMessageSenderOptions.cs

namespace WebPWrecover.Services;

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

配置 SendGrid 用户机密

使用机密管理工具设置 SendGridKey。 例如:

dotnet user-secrets set SendGridKey <SG.key>

Successfully saved SendGridKey = SG.keyVal to the secret store.

在 Windows 上,机密管理器将密钥/值对存储在 %APPDATA%/Microsoft/UserSecrets/<WebAppName-userSecretsId> 目录中的“secrets.json”文件中。

secrets.json”文件的内容未加密。 以下标记显示“secrets.json”文件。 已删除 SendGridKey 值。

{
  "SendGridKey": "<key removed>"
}

有关详细信息,请参阅选项模式配置

安装 SendGrid

本教程介绍如何通过 SendGrid 添加电子邮件通知,但你可以使用 SMTP 和其他机制发送电子邮件。

安装 SendGrid NuGet 包:

在包管理器控制台中输入以下命令:

Install-Package SendGrid

请参阅免费开始使用 SendGrid 以注册免费的 SendGrid 帐户。

实现 IEmailSender

使用类似于下面的代码创建 Services/EmailSender.cs 以实施 IEmailSender

using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;

namespace WebPWrecover.Services;

public class EmailSender : IEmailSender
{
    private readonly ILogger _logger;

    public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor,
                       ILogger<EmailSender> logger)
    {
        Options = optionsAccessor.Value;
        _logger = logger;
    }

    public AuthMessageSenderOptions Options { get; } //Set with Secret Manager.

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

    public async Task Execute(string apiKey, string subject, string message, string toEmail)
    {
        var client = new SendGridClient(apiKey);
        var msg = new SendGridMessage()
        {
            From = new EmailAddress("Joe@contoso.com", "Password Recovery"),
            Subject = subject,
            PlainTextContent = message,
            HtmlContent = message
        };
        msg.AddTo(new EmailAddress(toEmail));

        // Disable click tracking.
        // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
        msg.SetClickTracking(false, false);
        var response = await client.SendEmailAsync(msg);
        _logger.LogInformation(response.IsSuccessStatusCode 
                               ? $"Email to {toEmail} queued successfully!"
                               : $"Failure Email to {toEmail}");
    }
}

配置启动以支持电子邮件

在“Startup.cs”文件的 ConfigureServices 方法中添加以下代码:

  • EmailSender 添加为临时服务。
  • 注册 AuthMessageSenderOptions 配置实例。
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using WebPWrecover.Data;
using WebPWrecover.Services;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

搭建 RegisterConfirmation

按照为 Identity 搭建基架中的说明操作并为 Account\RegisterConfirmation 搭建基建。

当 Account.RegisterConfirmation 已搭建基架时禁用默认帐户验证

此部分仅适用于 Account.RegisterConfirmation 已搭建基架的情况。 如果 Account.RegisterConfirmation 尚未搭建基架,则请跳过此部分。

会将用户重定向到 Account.RegisterConfirmation,用户可以从中选择一个链接来确认帐户。 默认值 Account.RegisterConfirmation 仅用于测试,应在生产应用中禁用自动帐户验证

若要要求使用已确认的帐户,并防止注册时立即登录,请在已搭建基架的“/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs”文件中设置 DisplayConfirmAccountLink = false

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable

using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;

namespace WebPWrecover.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterConfirmationModel : PageModel
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly IEmailSender _sender;

        public RegisterConfirmationModel(UserManager<IdentityUser> userManager, IEmailSender sender)
        {
            _userManager = userManager;
            _sender = sender;
        }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public string Email { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public bool DisplayConfirmAccountLink { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public string EmailConfirmationUrl { get; set; }

        public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
        {
            if (email == null)
            {
                return RedirectToPage("/Index");
            }
            returnUrl = returnUrl ?? Url.Content("~/");

            var user = await _userManager.FindByEmailAsync(email);
            if (user == null)
            {
                return NotFound($"Unable to load user with email '{email}'.");
            }

            Email = email;
            // Once you add a real email sender, you should remove this code that lets you confirm the account
            DisplayConfirmAccountLink = false;
            if (DisplayConfirmAccountLink)
            {
                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                EmailConfirmationUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                    protocol: Request.Scheme);
            }

            return Page();
        }
    }
}

仅当 Account.RegisterConfirmation 已搭建基架时才需要执行此步骤。 未搭建基架的 RegisterConfirmation 会自动检测何时使用依赖项注入容器实施并注册的 IEmailSender

注册、确认电子邮件并重置密码

运行 Web 应用,并测试帐户确认和密码恢复流。

  • 运行应用并注册新用户
  • 查看电子邮件中的帐户确认链接。 如果未收到电子邮件,请参阅调试电子邮件
  • 单击链接以确认你的电子邮件。
  • 使用电子邮件和密码登录。
  • 注销。

测试密码重置

  • 如果已登录,请选择“注销”。
  • 选择“登录”链接,然后选择“忘记了密码?”链接。
  • 输入你注册该帐户时使用的电子邮件。
  • 系统将发送一封包含密码重置链接的电子邮件。 查看你的电子邮件,然后单击链接以重置密码。 密码重置成功后,可使用电子邮件和新密码登录。

重新发送电子邮件确认

在 ASP.NET Core 5.0 及更高版本中,选择“登录”页上的“重新发送电子邮件确认”链接。

更改电子邮件和活动超时

默认的非活动超时为 14 天。 下面的代码将非活动超时设置为 5 天:

services.ConfigureApplicationCookie(o => {
    o.ExpireTimeSpan = TimeSpan.FromDays(5);
    o.SlidingExpiration = true;
});

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

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

public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
                  options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.Configure<DataProtectionTokenProviderOptions>(o =>
       o.TokenLifespan = TimeSpan.FromHours(3));

    services.AddTransient<IEmailSender, EmailSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.AddRazorPages();
}

内置 Identity 用户令牌(请参阅 AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs)在 1 天后超时

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

Identity 用户令牌的默认令牌有效期是 1 天。 本部分介绍如何更改电子邮件令牌的使用期限。

添加自定义 DataProtectorTokenProvider<TUser>DataProtectionTokenProviderOptions

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 void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(config =>
    {
        config.SignIn.RequireConfirmedEmail = true;
        config.Tokens.ProviderMap.Add("CustomEmailConfirmation",
            new TokenProviderDescriptor(
                typeof(CustomEmailConfirmationTokenProvider<IdentityUser>)));
        config.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";
      }).AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddTransient<CustomEmailConfirmationTokenProvider<IdentityUser>>();

    services.AddTransient<IEmailSender, EmailSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.AddRazorPages();
}

调试电子邮件

如果无法使用电子邮件:

  • EmailSender.Execute 中设置断点,以验证是否调用 SendGridClient.SendEmailAsync
  • 使用类似于 EmailSender.Execute 的代码创建控制台应用来发送电子邮件
  • 查看电子邮件活动页。
  • 查看垃圾邮件文件夹。
  • 尝试在 Microsoft、Yahoo、Gmail 等不同的电子邮件提供程序上使用另一个电子邮件别名
  • 尝试发送到不同的电子邮件帐户。

安全最佳做法是在测试和开发中避免使用生产机密。 如果将应用发布到 Azure,请在 Azure Web 应用门户中将“SendGrid 机密”设置为“应用程序设置”。 配置系统设置为从环境变量读取密钥。

合并社交和本地登录帐户

若要完成本部分,必须首先启用外部身份验证提供程序。 请参阅 Facebook、Google 和外部提供程序身份验证

可通过单击电子邮件链接来合并本地帐户和社交帐户。 在下面的序列中,“RickAndMSFT@gmail.com”首先创建为本地登录;但也可以先将帐户创建为社交登录,然后添加本地登录。

Web application: RickAndMSFT@gmail.com user authenticated

单击“管理”链接。 请注意,与此帐户关联的外部登录(社交登录)为 0。

Manage view

单击另一个登录服务链接并接受应用请求。 下图中,Facebook 是外部身份验证提供程序:

Manage your external logins view listing Facebook

这两个帐户已合并。 可使用任一帐户登录。 你可能希望用户添加本地帐户,以防其社交登录身份验证服务关闭,或者更有可能失去其社交帐户的访问权限。

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

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

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