65.9K
CodeProject 正在变化。 阅读更多。
Home

在 ASP.NET Core 2.2 中要求已确认的电子邮件 - 第 2 部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2018 年 12 月 22 日

MIT

2分钟阅读

viewsIcon

22924

downloadIcon

441

在新的 ASP.NET Core 2.2 Razor 页面模板中基架和修改 Identity

引言

ASP.NET Core 2.2 Web 应用程序的第 2 部分,共 2 部分,允许用户更新已确认的电子邮件。 以下是允许用户更新其电子邮件的步骤。

在 ASP.NET Core 2.2 中要求已确认的电子邮件 - 第 1 部分

必备组件

  • .NET Core 2.2 SDK
  • 以下之一
    • Visual Studio 2017 15.9 或更高版本
    • Visual Studio for Mac 7.7 或更高版本
    • Visual Studio Code C# 扩展 1.17.1 或更高版本

您可以下载 VS 2017 项目,或者完成第 1 部分中的步骤后,按照这些步骤修改您自己的项目。

步骤 1 – 将 UnconfirmedEmail 属性添加到 IdentityUser

Entities 文件夹中添加名为 ApplicationUser 的新类

using Microsoft.AspNetCore.Identity;

namespace <YourProjectName>.Entities
{
    public class ApplicationUser : IdentityUser
    {
        [PersonalData]
        public string UnconfirmedEmail { get; set; }
    }
}

使用 查找和替换 在当前项目中将 <IdentityUser> 替换为 <ApplicationUser>

编辑 Startup.cs > ConfigureServices 以使用 ApplicationUser

    services.AddIdentity<ApplicationUser, IdentityRole>

编辑 Areas\Identity\Pages\Account\Manage\EnableAuthenticator.cshtml.cs

    private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user)

编辑 Areas\Identity\Pages\Account\Manage\DownloadPersonalData.cshtml.cs

    var personalDataProps = typeof(ApplicationUser).GetProperties().Where(
                            prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));

编辑 Areas\Identity\Pages\Account\ExternalLogin.cshtml.cs

    var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };

编辑 Register.cshtml.cs

    var user = new ApplicationUser { UserName = Input.UserName, Email = Input.Email };

解决您替换 IdentityUser 的命名空间问题。

using <YourProjectName>.Entities;

或者用于 cshtml

@using <YourProjectName>.Entities;

生成项目并检查错误。

步骤 2 - 更新数据库

Data 文件夹中编辑 ApplicationDbContext,添加 ApplicationUser

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>

从 VS 2017 的程序包管理器控制台中运行命令 "Add-Migration UnconfirmedEmail"。

运行命令 "Update-Database"。

步骤 3 – 添加更改电子邮件页面

编辑 ManageNavPages.cs,在 ChangePassword 属性上方添加

    public static string ChangeEmail => "ChangeEmail";

    public static string ChangeEmailNavClass(ViewContext viewContext) => 
                                             PageNavClass(viewContext, ChangeEmail);

编辑 _ManageNav.cshtml,在 Profile 项目下方添加

    <li class="nav-item">
    <a class="nav-link @ManageNavPages.ChangeEmailNavClass(ViewContext)" 
     id="change-email" asp-page="./ChangeEmail">Email</a></li>

Areas\Identity\Pages\Account\Manage 中创建一个名为 ChangeEmail 的 razor 页面。

编辑 ChangeEmail.cshtml

@page
@model ChangeEmailModel
@{
    ViewData["Title"] = "Change Email";
    ViewData["ActivePage"] = ManageNavPages.ChangeEmail;
}

<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />

<div class="row">
    <div class="col-md-6">
        <form id="change-email-form" method="post">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Email"></label>
                <input asp-for="Email" class="form-control" disabled />
            </div>

            <h5>New email needs to be verified.</h5>
            <div class="form-group">
                <label asp-for="Input.Email"></label>
                <input asp-for="Input.Email" class="form-control" />
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-primary">Update Email</button>
        </form>
    </div>
</div>

@section Scripts {
<partial name="_ValidationScriptsPartial" />

}

编辑 ChangeEmail.cshtml.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using <YourProjectName>.Services;
using <YourProjectName>.Entities;

namespace <YourProjectName>.Areas.Identity.Pages.Account.Manage
{
    public class ChangeEmailModel : PageModel
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly ILogger<ChangeEmailModel> _logger;
        private readonly IEmailSender _emailSender;

        public ChangeEmailModel(
            UserManager<ApplicationUser> userManager,
            SignInManager<ApplicationUser> signInManager,
            ILogger<ChangeEmailModel> logger,
            IEmailSender emailSender)
        {
            _userManager = userManager;
            _signInManager = signInManager;
            _logger = logger;
            _emailSender = emailSender;
        }

        [BindProperty]
        public InputModel Input { get; set; }

        [TempData]
        [Display(Name = "Verified Email")]
        public string Email { get; set; }

        [TempData]
        public string StatusMessage { get; set; }

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

        public async Task<IActionResult> OnGetAsync()
        {
            var user = await _userManager.GetUserAsync(User);
            if (user == null)
            {
                return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
            }

            var email = await _userManager.GetEmailAsync(user);

            Email = email;

            return Page();
        }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            var user = await _userManager.GetUserAsync(User);
            if (user == null)
            {
                return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
            }

            var email = await _userManager.GetEmailAsync(user);
            if (Input.Email != email)
            {
                var errors = new List<IdentityError>();
                if (_userManager.Options.User.RequireUniqueEmail)
                {
                    var owner = await _userManager.FindByEmailAsync(Input.Email);
                    if (owner != null && !string.Equals
                       (await _userManager.GetUserIdAsync(owner), 
                        await _userManager.GetUserIdAsync(user)))
                    {
                        ModelState.AddModelError(string.Empty, 
                        new IdentityErrorDescriber().DuplicateEmail(Input.Email).Description);
                        return Page();
                    }
                }

                var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
                if (!setEmailResult.Succeeded)
                {
                    var userId = await _userManager.GetUserIdAsync(user);
                    throw new InvalidOperationException($"Unexpected error occurred 
                                       setting email for user with ID '{userId}'.");
                }

                if (Input.Email.ToUpper() != email.ToUpper())
                {
                    var result = await _userManager.UpdateSecurityStampAsync(user);
                    if (!result.Succeeded)
                    {
                        foreach (var error in result.Errors)
                        {
                            ModelState.AddModelError(string.Empty, error.Description);
                            return Page();
                        }
                    }

                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);

                    var callbackUrl = Url.Page(
                        "/Account/ConfirmEmail",
                        pageHandler: null,
                        values: new { userId = user.Id, code = code },
                        protocol: Request.Scheme);

                    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                        $"Please confirm your account by 
                        <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                    _logger.LogInformation("User updated their UnconfirmedEmail.");
                    StatusMessage = "Please check your inbox to confirm the new email.";

                }
                else
                {
                    _logger.LogInformation("User updated their Email.");
                    StatusMessage = "Your email has been updated.";
                }
            }

            return RedirectToPage();
        }
    }
}

步骤 4 – 修改个人资料

Areas\Identity\Pages\Account\Manage 中编辑 Index.cshtml.cs 以使用新的 ChangeEmail 页面。

Add

    public string Email { get; set; }

移除

    public bool IsEmailConfirmed { get; set; }

InputModel 中删除

    [Required]
    [EmailAddress]
    public string Email { get; set; }

OnGetAsync > Input 中删除

    Email = email,

OnGetAsync 中删除

    IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);

OnPostAsync 中删除

    var email = await _userManager.GetEmailAsync(user);
    if (Input.Email != email)
    {
        var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
        if (!setEmailResult.Succeeded)
        {
            var userId = await _userManager.GetUserIdAsync(user);
            throw new InvalidOperationException($"Unexpected error occurred 
                         setting email for user with ID '{userId}'.");
         }
     }

移除

    public async Task<IActionResult> OnPostSendVerificationEmailAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '
            {_userManager.GetUserId(User)}'.");
        }


        var userId = await _userManager.GetUserIdAsync(user);
        var email = await _userManager.GetEmailAsync(user);
        var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
        var callbackUrl = Url.Page(
            "/Account/ConfirmEmail",
            pageHandler: null,
            values: new { userId = userId, code = code },
            protocol: Request.Scheme);
        await _emailSender.SendEmailAsync(
            email,
            "Confirm your email",
            $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode
                                             (callbackUrl)}' >clicking here</a>.");

        StatusMessage = "Verification email sent. Please check your email.";
        return RedirectToPage();
    }

编辑 Index.cshtml

替换

@if (Model.IsEmailConfirmed)
{
<div class="input-group">
<input asp-for="Input.Email" class="form-control" />

<span class="input-group-addon" aria-hidden="true">
<span class="glyphicon glyphicon-ok text-success"></span></span>
</div>
}
else
{
<input asp-for="Input.Email" class="form-control" />

<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" 
class="btn btn-link">Send verification email</button>
}
<span asp-validation-for="Input.Email" class="text-danger"></span>

有了

<input asp-for="Email" class="form-control" disabled />

步骤 5 – 覆盖 UserManager

Services 文件夹中添加名为 ApplicationUserManager 的新类

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using <YourProjectName>.Entities;

namespace <YourProjectName>.Services
{
    public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(IUserStore<ApplicationUser> store,
            IOptions<IdentityOptions> optionsAccessor,
            IPasswordHasher<ApplicationUser> passwordHasher,
            IEnumerable<IUserValidator<ApplicationUser>> userValidators,
            IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators,
            ILookupNormalizer keyNormalizer,
            IdentityErrorDescriber errors,
            IServiceProvider services,
            ILogger<UserManager<ApplicationUser>> logger)
            : base(store, optionsAccessor, passwordHasher, userValidators,
                  passwordValidators, keyNormalizer, errors, services, logger)
        {
        }

        /// <summary>
        /// Sets the <paramref name="email"/> address for a <paramref name="user"/>.
        /// </summary>
        /// <param name="user">The user whose email should be set.</param>
        /// <param name="email">The email to set.</param>
        /// <returns>
        /// The <see cref="Task"/> that represents the asynchronous operation, 
        /// containing the <see cref="IdentityResult"/>
        /// of the operation.
        /// </returns>
        public override async Task<IdentityResult> SetEmailAsync(ApplicationUser user, string email)
        {
            ThrowIfDisposed();
            if (user == null)
            {
                throw new ArgumentNullException(nameof(user));
            }

            if (user.EmailConfirmed && user.Email.ToUpper() != email.ToUpper())
                user.UnconfirmedEmail = email;
            else
                user.Email = email;

            return await UpdateUserAsync(user);
        }

        /// <summary>
        /// Validates that an email confirmation token matches the specified 
        /// <paramref name="user"/> and if successful sets
        /// EmailConfirmed to true and if UnconfirmedEmail is not NULL or Empty, 
        /// copies the user's UnconfirmedEmail to user's
        /// Email and sets UnconfirmedEmail to NULL.
        /// </summary>
        /// <param name="user">The user to validate the token against.</param>
        /// <param name="token">The email confirmation token to validate.</param>
        /// <returns>
        /// The <see cref="Task"/> that represents the asynchronous operation, 
        /// containing the <see cref="IdentityResult"/>
        /// of the operation.
        /// </returns>
        public override async Task<IdentityResult> 
                  ConfirmEmailAsync(ApplicationUser user, string token)
        {
            ThrowIfDisposed();
            if (user == null)
            {
                throw new ArgumentNullException(nameof(user));
            }

            IdentityResult result;
            var provider = Options.Tokens.EmailConfirmationTokenProvider;
            var isValidToken = await base.VerifyUserTokenAsync
                               (user, provider, "EmailConfirmation", token);

            if (isValidToken)
            {
                if (!string.IsNullOrEmpty(user.UnconfirmedEmail))
                {
                    user.Email = user.UnconfirmedEmail;
                    user.UnconfirmedEmail = null;
                }
                user.EmailConfirmed = true;
                result = await UpdateUserAsync(user);
            }
            else
            {
                result = IdentityResult.Failed(new IdentityErrorDescriber().InvalidToken());
            }

            return result;
        }
    }
}

编辑 Startup.cs > ConfigureServices,添加 .AddUserManager<ApplicationUserManager>()

    services.AddIdentity<ApplicationUser, IdentityRole>(config =>
        {
            config.SignIn.RequireConfirmedEmail = true;
            config.User.RequireUniqueEmail = true;
        })
        .AddDefaultUI(UIFramework.Bootstrap4)
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddUserManager<ApplicationUserManager>()
        .AddDefaultTokenProviders();

构建并测试项目。

关注点

我不确定在基架 Identity 时是否必须“覆盖所有文件”,但我更喜欢检查并完全控制用户的体验。

请注意 UpdateSecurityStampAsync(user)GenerateEmailConfirmationTokenAsync(user) 之前。 这会使发送给用户的任何以前的代码无效。

历史

  • 2018-12-21:初始帖子
  • 2018-12-22:更新历史记录日期
© . All rights reserved.