Подтверждение по Email в Identity

Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7

Последнее обновление: 31.12.2020

Рассмотрим, как добавить в ASP.NET Core Identity механизм подтверждения Email.

Итак, создадим новый проект ASP.NET Core с типом ASP.NET Core Web App (Model-View-Controller) без аутентификации.

Подтверждение email в ASP.NET Core

Для взаимодействия с MS SQL Server через ASP.NET Core Identity добавим в проект через Nuget пакеты Microsoft.AspNetCore.Identity.EntityFrameworkCore и Microsoft.EntityFrameworkCore.SqlServer.

Для отправки email будем использовать MailKit. Для его установки добавим через NuGet одноименный пакет:

MailKit и отправка писем в ASP.NET Core MVC

После создания проекта определим некотоый базовый функционал для аутентификации и авторизации в приложении. Пржде всего в папке Models определим класс пользователя User:


using Microsoft.AspNetCore.Identity;

namespace EmailApp.Models
{
    public class User : IdentityUser
    {
	}
}

И также в папке Models определим класс контекста данных ApplicationContext:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace EmailApp.Models
{
    public class ApplicationContext : IdentityDbContext<User>
    {
        public ApplicationContext(DbContextOptions<ApplicationContext> options)
            : base(options)
        {
            Database.EnsureCreated();
        }
    }
}

Далее создадим в проекте папку ViewModels и определим в ней для логина и регистрации две модели RegisterViewModel и LoginViewModel для регистрации и входа в приложение соответственно:

using System.ComponentModel.DataAnnotations;

namespace EmailApp.ViewModels
{
    public class RegisterViewModel
    {
        [Required]
        [Display(Name = "Email")]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Пароль")]
        public string Password { get; set; }

        [Required]
        [Compare("Password", ErrorMessage = "Пароли не совпадают")]
        [DataType(DataType.Password)]
        [Display(Name = "Подтвердить пароль")]
        public string PasswordConfirm { get; set; }
    }
	public class LoginViewModel
    {
        [Required]
        [Display(Name = "Email")]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Пароль")]
        public string Password { get; set; }

        [Display(Name = "Запомнить?")]
        public bool RememberMe { get; set; }

        public string ReturnUrl { get; set; }
    }
}

В папке Views создадим каталог Account и определим в нем представление Register.cshtml для регистрации:

@model EmailApp.ViewModels.RegisterViewModel
<h2>Регистрация нового пользователя</h2>
<form method="post" asp-controller="Account" asp-action="Register">
    <div asp-validation-summary="ModelOnly"></div>
    <div>
        <label asp-for="Email"></label><br />
        <input asp-for="Email" />
        <span asp-validation-for="Email"></span>
    </div>
    <div>
        <label asp-for="Password"></label><br />
        <input asp-for="Password" />
        <span asp-validation-for="Password"></span>
    </div>
    <div>
        <label asp-for="PasswordConfirm"></label><br />
        <input asp-for="PasswordConfirm" />
        <span asp-validation-for="PasswordConfirm"></span>
    </div>
    <div>
        <input type="submit" value="Регистрация" />
    </div>
</form>

И там же определим представление Login.cshtml для логина:

@model EmailApp.ViewModels.LoginViewModel

<h2>Вход в приложение</h2>
<form method="post" asp-controller="Account" asp-action="Login"
      asp-route-returnUrl="@Model.ReturnUrl">
    <div asp-validation-summary="ModelOnly"></div>
    <div>
        <label asp-for="Email"></label><br />
        <input asp-for="Email" />
        <span asp-validation-for="Email"></span>
    </div>
    <div>
        <label asp-for="Password"></label><br />
        <input asp-for="Password" />
        <span asp-validation-for="Password"></span>
    </div>
    <div>
        <label asp-for="RememberMe"></label><br />
        <input asp-for="RememberMe" />
    </div>
    <div>
        <input type="submit" value="Войти" />
    </div>
</form>

Вначале добавим в проект новый класс EmailService, через который будет производиться отправка:

using MimeKit;
using MailKit.Net.Smtp;
using System.Threading.Tasks;
namespace EmailApp
{
    public class EmailService
    {
        public async Task SendEmailAsync(string email, string subject, string message)
        {
            var emailMessage = new MimeMessage();

            emailMessage.From.Add(new MailboxAddress("Администрация сайта", "admin@metanit.com"));
            emailMessage.To.Add(new MailboxAddress("", email));
            emailMessage.Subject = subject;
            emailMessage.Body = new TextPart(MimeKit.Text.TextFormat.Html)
            {
                Text = message
            };
            
            using (var client = new SmtpClient())
            {
                await client.ConnectAsync("smtp.metanit.com", 465, true);
                await client.AuthenticateAsync("admin@metanit.com", "password");
                await client.SendAsync(emailMessage);

                await client.DisconnectAsync(true);
            }
        }
    }
}

В зависимости конкретные настойки - адрес smtp-сервера, номер порта, нужен ли ssl - могут отличаться. Также в зависимости от выбранного почтового сервиса, возможно, потребуется также установить соответствующие разрешения или какие-то дополнительные настройки в самом почтовом сервисе - в каждом сервисе (google, mail.ru, yandex, yahoo) они могут отличаться. Поэтому в идеале лучше, конечно, использовать какой-то специальный smtp-сервер.

Теперь используем этот класс. Для этого в папке Controllers определим новый контроллер AccountController:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using EmailApp.ViewModels;
using EmailApp.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authorization;

namespace EmailApp.Controllers
{
    public class AccountController : Controller
    {
        private readonly UserManager<User> _userManager;
        private readonly SignInManager<User> _signInManager;

        public AccountController(UserManager<User> userManager, SignInManager<User> signInManager)
        {
            _userManager = userManager;
            _signInManager = signInManager;
        }
        [HttpGet]
        public IActionResult Register()
        {
            return View();
        }
        [HttpPost]
        public async Task<IActionResult> Register(RegisterViewModel model)
        {
            if (ModelState.IsValid)
            {
                User user = new User { Email = model.Email, UserName = model.Email };
                // добавляем пользователя
                var result = await _userManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    // генерация токена для пользователя
                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    var callbackUrl = Url.Action(
                        "ConfirmEmail",
                        "Account",
                        new { userId = user.Id, code = code },
                        protocol: HttpContext.Request.Scheme);
                    EmailService emailService = new EmailService();
                    await emailService.SendEmailAsync(model.Email, "Confirm your account",
                        $"Подтвердите регистрацию, перейдя по ссылке: <a href='{callbackUrl}'>link</a>");

                    return Content("Для завершения регистрации проверьте электронную почту и перейдите по ссылке, указанной в письме");
                }
                else
                {
                    foreach (var error in result.Errors)
                    {
                        ModelState.AddModelError(string.Empty, error.Description);
                    }
                }
            }
            return View(model);
        }

        [HttpGet]
        [AllowAnonymous]
        public async Task<IActionResult> ConfirmEmail(string userId, string code)
        {
            if (userId == null || code == null)
            {
                return View("Error");
            }
            var user = await _userManager.FindByIdAsync(userId);
            if (user == null)
            {
                return View("Error");
            }
            var result = await _userManager.ConfirmEmailAsync(user, code);
            if(result.Succeeded)
                return RedirectToAction("Index", "Home");
            else
                return View("Error");
        }

        [HttpGet]
        public IActionResult Login(string returnUrl = null)
        {
            return View(new LoginViewModel { ReturnUrl = returnUrl });
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginViewModel model)
        {
            if (ModelState.IsValid)
            {
                var user = await _userManager.FindByNameAsync(model.Email);
                if (user != null)
                {
                    // проверяем, подтвержден ли email
                    if (!await _userManager.IsEmailConfirmedAsync(user))
                    {
                        ModelState.AddModelError(string.Empty, "Вы не подтвердили свой email");
                        return View(model);
                    }
                }

                var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, false);
                if (result.Succeeded)
                {
                    return RedirectToAction("Index", "Home");
                }
                else
                {
                    ModelState.AddModelError("", "Неправильный логин и (или) пароль");
                }
            }
            return View(model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> LogOff()
        {
            // удаляем аутентификационные куки
            await _signInManager.SignOutAsync();
            return RedirectToAction("Index", "Home");
        }
    }
}

При регистрации в методе Register с помощью вызова _userManager.GenerateEmailConfirmationTokenAsync() для указанного пользователя генерируется определенный код или токен. Далее с помощью этого кода формируется ссылка, которая отправляется в письме. Причем генерируемый код в базе данных не хранится.

Здесь ожидается, что пользователь, перейдя по ссылке, обратится к методу ConfirmEmail. В этом методе с помощью вызова _userManager.ConfirmEmailAsync(user, code) проверяем соответствие токена пользователю. В случае удачного соответствия у пользователя в базе данных для поля EmailConfirmed выставляется значение true. Благодаря чему в дальнейшем мы можем разгранизивать пользователей, который подтвердили электронный адрес и которые не сделали этого. В частности, в методе Login при входе пользователя на сайт мы можем проверить с помощью выражения await _userManager.IsEmailConfirmedAsync(user) подтвердил ли пользователь электронную почту и в зависимости от результатов проверки выполнить те или иные действия.

В конце определим необходимый код в классе Startup:

using EmailApp.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace EmailApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            string conStr = "Server=(localdb)\\mssqllocaldb;Database=usersdb46;Trusted_Connection=True;MultipleActiveResultSets=true";
            services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(conStr));

            services.AddIdentity<User, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationContext>()
                .AddDefaultTokenProviders();
            services.AddControllersWithViews();
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseDeveloperExceptionPage();
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

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

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

Здесь следует обратить внимание на вызов метода AddDefaultTokenProviders() в методе ConfigureServices. Благодаря этому вызову добавляется функциональность генерации токенов, которые отсылаются в письме для подтверждения. В принципе мы можем и не использовать этот метод - AddDefaultTokenProviders(), однако тогда нам придется самостоятельно реализовать интерфейс IUserTwoFactorTokenProvider, методы которого собственно и отвечают за генерацию и валидацию токена подтверждения электронной почты.

Теперь запустим проект и зарегистрируемся.

Аутентификация через Email в ASP.NET Core

После этого на введенную электронную почту отправится письмо с ссылкой. Перейдем по ссылке для подтверждения регистрации.

На уровне модели это выражается в установке свойства EmailConfirmed: после регистрации и отправки сообщения на электронную почту это свойство получает значение false. А после перехода по ссылке и тем самым подтверждения регистрации ему присваивается значение true. Таким образом, используя данное свойство, мы можем контроллировать доступ для тех, кто подтвердил и кто не подтвердил регистрацию.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850