共用方式為


使用 ASP.NET Core 在 ASP.NET Core Blazor WebAssembly 中進行帳戶確認和密碼復原 Identity

注意

這不是本文的最新版本。 如需目前版本,請參閱本文的 .NET 9 版本

重要

這項資訊與發行前版本產品有關,在正式發行前可能會大幅修改。 Microsoft未就此處提供的資訊提供任何明示或默示擔保。

如需目前版本,請參閱本文的 .NET 9 版本

本文說明如何使用電子郵件確認和密碼復原,使用 ASP.NET Core 設定 ASP.NET Core Blazor WebAssemblyIdentity 應用程式。

注意

本文僅適用於具有 ASP.NET Core Blazor WebAssembly的獨立Identity應用程式。 若要實作 Blazor Web App的電子郵件確認和密碼復原,請參閱 ASP.NET Core Blazor中的帳戶確認和密碼復原。

命名空間和文章範例程式碼

本文範例所使用的命名空間如下:

  • Backend 適用於本文中的後端伺服器 Web API 專案(「伺服器專案」)。
  • BlazorWasmAuth 針對前端客戶端獨立 Blazor WebAssembly 應用程式 (本文中的「用戶端專案」)。

這些命名空間會對應至 GitHub 存放庫中範例解決方案中的BlazorWebAssemblyStandaloneWithIdentitydotnet/blazor-samples專案。 如需詳細資訊,請參閱使用 ASP.NET Core 保護 ASP.NET CoreBlazor WebAssemblyIdentity

如果您未使用 BlazorWebAssemblyStandaloneWithIdentity 範例解決方案,請將程式代碼範例中的命名空間變更為使用專案的命名空間。

選取並設定伺服器項目的電子郵件提供者

本文中, Mailchimp 的交易式 API 是透過 Mandrill.net 來傳送電子郵件。 我們建議使用電子郵件服務來傳送電子郵件,而不是 SMTP。 SMTP 難以正確設定及保護。 無論您使用哪一個電子郵件服務、存取其 .NET 應用程式的指導、建立帳戶、為其服務設定 API 金鑰,以及安裝所需的任何 NuGet 套件。

在伺服器專案中,建立類別來保存秘密電子郵件提供者 API 金鑰。 本文中的範例會使用名為 AuthMessageSenderOptionsEmailAuthKey 類別搭配 屬性來保存索引鍵。

AuthMessageSenderOptions.cs

namespace Backend;

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

AuthMessageSenderOptions 伺服器項目的 Program 檔案中註冊組態實例:

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

設定電子郵件提供者安全性金鑰的機密信息

從提供者接收電子郵件提供者的安全性密鑰,並在下列指引中使用。

使用下列其中一種方法或兩種方法,將秘密提供給應用程式:

  • Secret Manager 工具:秘密管理員工具會將私人數據儲存在本機電腦上,而且只會在本機開發期間使用。
  • Azure Key Vault:您可以將秘密儲存在密鑰保存庫中,以用於任何環境,包括在本機工作時用於開發環境。 有些開發人員偏好使用金鑰保存庫進行預備和生產部署,並使用 Secret Manager 工具 進行本機開發。

強烈建議您避免將秘密儲存在專案程式代碼或組態檔中。 使用安全驗證流程,例如本節中任一或兩種方法。

秘密管理員工具

如果伺服器項目已經針對秘密管理員工具初始化,它就會在其專案檔中已經有應用程式秘密識別碼 (<AppSecretsId>.csproj) 。 在 Visual Studio 中,您可以查看專案在 方案總管選取專案時,查看 [屬性] 面板,來判斷應用程式秘密標識元是否存在。 如果應用程式尚未初始化,請在開啟至伺服器專案目錄的命令殼層中執行下列命令。 在 Visual Studio 中,您可以使用開發人員 PowerShell 命令提示字元(在開啟命令殼層之後,使用 cd 命令將目錄變更為伺服器專案)。

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 應該使用安全驗證流程,以避免在專案程式代碼或組態檔內維護認證。 在本機開發測試之外,建議您避免使用環境變數來儲存敏感數據,因為環境變數不是最安全的方法。 針對本機開發測試, 建議使用秘密管理員工具 來保護敏感數據。 如需詳細資訊,請參閱 安全地維護敏感數據和認證

Azure Key Vault

Azure Key Vault 提供一種將應用程式用戶端密碼安全傳遞到應用程式的方法。

若要建立密鑰保存庫並設定秘密,請參閱 關於 Azure Key Vault 秘密 (Azure 檔),這會跨鏈接資源以開始使用 Azure Key Vault。 若要在本節中實作程序代碼,請在建立密鑰保存庫和秘密時,記錄來自 Azure 的密鑰保存庫 URI 和秘密名稱。 當您在 存取原則 面板中設定秘密的存取原則時:

  • 只需要 取得 秘密許可權。
  • 選取應用程式作為秘密 主體

在 Azure 或 Entra 入口網站中確認應用程式已被授予您為電子郵件提供者密鑰所建立的秘密存取權。

重要

金鑰保存庫密碼會以到期日建立。 請務必追蹤金鑰保存庫秘密何時到期,並在該日期通過之前為應用程式建立新的秘密。

如果Microsoft Identity 套件還不屬於應用程式的套件註冊,請將下列套件新增至 Azure Identity 和 Azure Key Vault 的伺服器專案。 這些套件透過 Microsoft Identity Web 套件間接提供,因此,如果應用程式未參考 Microsoft.Identity.Web,您只需加入這些套件:

將下列 AzureHelper 類別新增至伺服器專案。 GetKeyVaultSecret 方法會從金鑰保存庫擷取秘密。 調整命名空間 (BlazorSample.Helpers),以符合您的專案命名空間配置。

Helpers/AzureHelper.cs

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

namespace BlazorSample.Helpers;

public static class AzureHelper
{
    public static string GetKeyVaultSecret(string tenantId, string vaultUri, string secretName)
    {
        DefaultAzureCredentialOptions options = new()
        {
            // Specify the tenant ID to use the dev credentials when running the app locally
            // in Visual Studio.
            VisualStudioTenantId = tenantId,
            SharedTokenCacheTenantId = tenantId
        };

        var client = new SecretClient(new Uri(vaultUri), new DefaultAzureCredential(options));
        var secret = client.GetSecretAsync(secretName).Result;

        return secret.Value.Value;
    }
}

在伺服器專案的 Program 檔案中註冊服務後,取得並綁定秘密到 配置選項

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

var emailAuthKey = AzureHelper.GetKeyVaultSecret(
    tenantId, vaultUri, "EmailAuthKey");

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

如果您想要控制上述程式代碼運作的環境,例如避免在本機執行程式碼,因為您選擇使用 Secret Manager 工具 進行本機開發,您可以將上述程式代碼包裝在檢查環境的條件語句中:

if (!context.HostingEnvironment.IsDevelopment())
{
    ...
}

在 [AzureAd] 區段中,如果尚未存在,您可能需要新增 appsettings.json 伺服器專案中的 appsettings.json,如果它們尚未存在,請新增下列 TenantId,並 VaultUri 組態索引鍵和值:

"AzureAd": {
  "TenantId": "{TENANT ID}",
  "VaultUri": "{VAULT URI}"
}

在上述範例中:

  • {TENANT ID} 佔位符是 Azure 中的應用程式租用戶識別碼。
  • {VAULT URI} 佔位符是金鑰保存庫 URI。 在 URI 中包含結尾斜線。

例:

"TenantId": "00001111-aaaa-2222-bbbb-3333cccc4444",
"VaultUri": "https://contoso.vault.azure.net/"

組態可用來根據應用程式的環境設定檔,提供專屬的金鑰保存庫和秘密名稱。 例如,您可以在開發時為 appsettings.Development.json 提供不同的組態值,在暫存時為 appsettings.Staging.json 提供不同的組態值,以及在生產部署時為 appsettings.Production.json 提供不同的組態值。 如需詳細資訊,請參閱 ASP.NET Core Blazor 組態

在伺服器項目中實 IEmailSender

下列範例是以 Mailchimp 的交易式 API 為基礎,使用 Mandrill.net。 對於不同的提供者,請參閱其如何實作傳送電子郵件訊息的檔。

Mandrill.net NuGet 套件新增至伺服器專案。

新增下列 EmailSender 類別以實作 IEmailSender<TUser>。 在下列範例中, AppUserIdentityUser。 您可以進一步自訂訊息 HTML 標記。 只要 message 傳遞至 以 MandrillMessage 字元開頭 < ,Mandrill.net API 就會假設訊息本文是以 HTML 撰寫。

EmailSender.cs

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

namespace Backend;

public class EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor,
    ILogger<EmailSender> logger) : IEmailSender<AppUser>
{
    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);
    }
}

注意

郵件內文內容可能需要電子郵件服務提供者的特殊編碼方式。 如果郵件本文中的鏈接無法在電子郵件訊息中追蹤,請參閱服務提供者的檔,以針對問題進行疑難解答。

將下列 IEmailSender<TUser> 服務註冊新增至伺服器項目的 Program 檔案:

builder.Services.AddTransient<IEmailSender<AppUser>, EmailSender>();

將伺服器項目設定為需要電子郵件確認

在伺服器項目的檔案中 Program ,需要確認的電子郵件位址才能登入應用程式。

找出呼叫 AddIdentityCore 並將 屬性設定為 RequireConfirmedEmailtrue行:

- builder.Services.AddIdentityCore<AppUser>()
+ builder.Services.AddIdentityCore<AppUser>(o => o.SignIn.RequireConfirmedEmail = true)

更新客戶端項目的帳戶註冊回應

在用戶端專案的 Register 元件中 ,Components/Identity/Register.razor將訊息變更為成功帳戶註冊上的使用者,以指示他們確認其帳戶。 下列範例包含一個連結,可在伺服器上觸發 Identity 以重新傳送確認電子郵件。

- You successfully registered and can <a href="login">login</a> to the app.
+ You successfully registered. You must now confirm your account by clicking 
+ the link in the email that was sent to you. After confirming your account, 
+ you can <a href="login">login</a> to the app. 
+ <a href="resendConfirmationEmail">Resend confirmation email</a>

更新種子數據程式代碼以確認植入的帳戶

在伺服器專案的種子數據類別中,SeedData.cs變更 方法中的 InitializeAsync 程式碼來確認植入的帳戶,這可避免使用其中一個帳戶對解決方案的每個測試回合要求電子郵件地址確認:

- if (appUser is not null && user.RoleList is not null)
- {
-     await userManager.AddToRolesAsync(appUser, user.RoleList);
- }
+ if (appUser is not null)
+ {
+     if (user.RoleList is not null)
+     {
+         await userManager.AddToRolesAsync(appUser, user.RoleList);
+     }
+ 
+     var token = await userManager.GenerateEmailConfirmationTokenAsync(appUser);
+     await userManager.ConfirmEmailAsync(appUser, token);
+ }

在網站有使用者之後啟用帳戶確認

在有使用者的網站上啟用帳戶確認,會鎖定所有現有的使用者。 現有的使用者因為未確認其帳戶而遭到鎖定。 使用下列其中一種方法,其超出本文的範圍:

  • 更新資料庫,將所有現有使用者標示為已確認。
  • 具有確認連結的批次傳送電子郵件給所有現有的使用者,這需要每個使用者確認自己的帳戶。

密碼復原

密碼復原需要伺服器應用程式採用電子郵件提供者,才能將密碼重設代碼傳送給使用者。 因此,請遵循本文稍早的指導方針來採用電子郵件提供者:

密碼復原是兩個步驟的程式:

  1. POST 要求是在 /forgotPassword 伺服器專案中提供的 MapIdentityApi 端點提出。 UI 中的訊息會指示使用者檢查其電子郵件是否有重設程式代碼。
  2. POST 要求會使用使用者的電子郵件地址、密碼重設代碼和新密碼,對伺服器專案的端點提出 /resetPassword

上述步驟示範範例解決方案的下列實作指引。

在客戶端專案中,將下列方法簽章新增至 IAccountManagement 類別 (Identity/IAccountManagement.cs):

public Task<bool> ForgotPasswordAsync(string email);

public Task<FormResult> ResetPasswordAsync(string email, string resetCode, 
    string newPassword);

在客戶端專案中,在 類別中新增上述方法的 CookieAuthenticationStateProvider 實作 (Identity/CookieAuthenticationStateProvider.cs):

public async Task<bool> ForgotPasswordAsync(string email)
{
    try
    {
        var result = await httpClient.PostAsJsonAsync(
            "forgotPassword", new
            {
                email
            });

        if (result.IsSuccessStatusCode)
        {
            return true;
        }
    }
    catch { }

    return false;
}

public async Task<FormResult> ResetPasswordAsync(string email, string resetCode, 
    string newPassword)
{
    string[] defaultDetail = [ "An unknown error prevented password reset." ];

    try
    {
        var result = await httpClient.PostAsJsonAsync(
            "resetPassword", new
            {
                email,
                resetCode,
                newPassword
            });

        if (result.IsSuccessStatusCode)
        {
            return new FormResult { Succeeded = true };
        }

        var details = await result.Content.ReadAsStringAsync();
        var problemDetails = JsonDocument.Parse(details);
        var errors = new List<string>();
        var errorList = problemDetails.RootElement.GetProperty("errors");

        foreach (var errorEntry in errorList.EnumerateObject())
        {
            if (errorEntry.Value.ValueKind == JsonValueKind.String)
            {
                errors.Add(errorEntry.Value.GetString()!);
            }
            else if (errorEntry.Value.ValueKind == JsonValueKind.Array)
            {
                errors.AddRange(
                    errorEntry.Value.EnumerateArray().Select(
                        e => e.GetString() ?? string.Empty)
                    .Where(e => !string.IsNullOrEmpty(e)));
            }
        }

        return new FormResult
        {
            Succeeded = false,
            ErrorList = problemDetails == null ? defaultDetail : [.. errors]
        };
    }
    catch { }

    return new FormResult
    {
        Succeeded = false,
        ErrorList = defaultDetail
    };
}

在客戶端專案中,新增下列 ForgotPassword 元件。

Components/Identity/ForgotPassword.razor

@page "/forgot-password"
@using System.ComponentModel.DataAnnotations
@using BlazorWasmAuth.Identity
@inject IAccountManagement Acct

<PageTitle>Forgot your password?</PageTitle>

<h1>Forgot your password?</h1>
<p>Provide your email address and select the <b>Reset password</b> button.</p>
<hr />
<div class="row">
    <div class="col-md-4">
        @if (!passwordResetCodeSent)
        {
            <EditForm Model="Input" FormName="forgot-password" 
                      OnValidSubmit="OnValidSubmitStep1Async" method="post">
                <DataAnnotationsValidator />
                <ValidationSummary class="text-danger" role="alert" />

                <div class="form-floating mb-3">
                    <InputText @bind-Value="Input.Email" 
                        id="Input.Email" class="form-control" 
                        autocomplete="username" aria-required="true" 
                        placeholder="name@example.com" />
                    <label for="Input.Email" class="form-label">
                        Email
                    </label>
                    <ValidationMessage For="() => Input.Email" 
                        class="text-danger" />
                </div>
                <button type="submit" class="w-100 btn btn-lg btn-primary">
                    Request reset code
                </button>
            </EditForm>
        }
        else
        {
            if (passwordResetSuccess)
            {
                if (errors)
                {
                    foreach (var error in errorList)
                    {
                        <div class="alert alert-danger">@error</div>
                    }
                }
                else
                {
                    <div>
                        Your password was reset. You may <a href="login">login</a> 
                        to the app with your new password.
                    </div>
                }
            }
            else
            {
                <div>
                    A password reset code has been sent to your email address. 
                    Obtain the code from the email for this form.
                </div>
                <EditForm Model="Reset" FormName="reset-password" 
                    OnValidSubmit="OnValidSubmitStep2Async" method="post">
                    <DataAnnotationsValidator />
                    <ValidationSummary class="text-danger" role="alert" />

                    <div class="form-floating mb-3">
                        <InputText @bind-Value="Reset.ResetCode" 
                            id="Reset.ResetCode" class="form-control" 
                            autocomplete="username" aria-required="true" />
                        <label for="Reset.ResetCode" class="form-label">
                            Reset code
                        </label>
                        <ValidationMessage For="() => Reset.ResetCode" 
                            class="text-danger" />
                    </div>
                    <div class="form-floating mb-3">
                        <InputText type="password" @bind-Value="Reset.NewPassword" 
                            id="Reset.NewPassword" class="form-control" 
                            autocomplete="new-password" aria-required="true" 
                            placeholder="password" />
                        <label for="Reset.NewPassword" class="form-label">
                            New Password
                        </label>
                        <ValidationMessage For="() => Reset.NewPassword" 
                            class="text-danger" />
                    </div>
                    <div class="form-floating mb-3">
                        <InputText type="password" 
                            @bind-Value="Reset.ConfirmPassword" 
                            id="Reset.ConfirmPassword" class="form-control" 
                            autocomplete="new-password" aria-required="true" 
                            placeholder="password" />
                        <label for="Reset.ConfirmPassword" class="form-label">
                            Confirm Password
                        </label>
                        <ValidationMessage For="() => Reset.ConfirmPassword" 
                            class="text-danger" />
                    </div>
                    <button type="submit" class="w-100 btn btn-lg btn-primary">
                        Reset password
                    </button>
                </EditForm>
            }
        }
    </div>
</div>

@code {
    private bool passwordResetCodeSent, passwordResetSuccess, errors;
    private string[] errorList = [];

    [SupplyParameterFromForm(FormName = "forgot-password")]
    private InputModel Input { get; set; } = new();

    [SupplyParameterFromForm(FormName = "reset-password")]
    private ResetModel Reset { get; set; } = new();

    private async Task OnValidSubmitStep1Async()
    {
        passwordResetCodeSent = await Acct.ForgotPasswordAsync(Input.Email);
    }

    private async Task OnValidSubmitStep2Async()
    {
        var result = await Acct.ResetPasswordAsync(Input.Email, Reset.ResetCode, 
            Reset.NewPassword);

        if (result.Succeeded)
        {
            passwordResetSuccess = true;

        }
        else
        {
            errors = true;
            errorList = result.ErrorList;
        }
    }

    private sealed class InputModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; } = string.Empty;
    }

    private sealed class ResetModel
    {
        [Required]
        [Base64String]
        public string ResetCode { get; set; } = string.Empty;

        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at " +
            "max {1} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string NewPassword { get; set; } = string.Empty;

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("NewPassword", ErrorMessage = "The new password and confirmation " +
            "password don't match.")]
        public string ConfirmPassword { get; set; } = string.Empty;
    }
}

Login在用戶端專案的元件 (Components/Identity/Login.razor) 緊接在結尾</NotAuthorized>標記之前,新增忘記的密碼連結以連線到ForgotPassword元件:

<div>
    <a href="forgot-password">Forgot password</a>
</div>

電子郵件和活動逾時

預設無活動逾時為 14 天。 在伺服器專案中,下列程式代碼會將無活動逾時設定為五天,並滑動到期:

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

變更所有 ASP.NET Core 資料保護權杖生命週期

在伺服器專案中,下列程式代碼會將數據保護令牌的逾時期間變更為三小時:

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 使用者權杖 的預設權杖生命週期為 一天

注意

.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<AppUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
        options.Tokens.ProviderMap.Add("CustomEmailConfirmation",
            new TokenProviderDescriptor(
                typeof(CustomEmailConfirmationTokenProvider<AppUser>)));
        options.Tokens.EmailConfirmationTokenProvider = 
            "CustomEmailConfirmation";
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

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

疑難排解

如果您無法讓電子郵件運作:

  • EmailSender.Execute 中設定中斷點以確認 SendEmailAsync 已呼叫。
  • 建立主控台應用程式,以使用與 EmailSender.Execute 類似的程式碼傳送電子郵件來偵錯問題回報。
  • 檢閱電子郵件提供者網站上的帳戶電子郵件記錄頁面。
  • 請務必檢查您的垃圾郵件資料夾是否有這些訊息。
  • 在不同的電子郵件提供者上嘗試另一個電子郵件別名,例如 Microsoft、Yahoo、Gmail 等。
  • 請嘗試傳送至不同的電子郵件帳戶。

其他資源