Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7
Рассмотрим, как добавить в ASP.NET Core Identity механизм подтверждения Email.
Итак, создадим новый проект ASP.NET Core с типом ASP.NET Core Web App (Model-View-Controller) без аутентификации.
Для взаимодействия с MS SQL Server через ASP.NET Core Identity добавим в проект через Nuget пакеты Microsoft.AspNetCore.Identity.EntityFrameworkCore и Microsoft.EntityFrameworkCore.SqlServer.
Для отправки email будем использовать MailKit. Для его установки добавим через NuGet одноименный пакет:
После создания проекта определим некотоый базовый функционал для аутентификации и авторизации в приложении. Пржде всего в папке 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, методы которого собственно и отвечают за
генерацию и валидацию токена подтверждения электронной почты.
Теперь запустим проект и зарегистрируемся.
После этого на введенную электронную почту отправится письмо с ссылкой. Перейдем по ссылке для подтверждения регистрации.
На уровне модели это выражается в установке свойства EmailConfirmed: после регистрации и отправки сообщения на
электронную почту это свойство получает значение false
. А после перехода по ссылке и тем самым подтверждения регистрации
ему присваивается значение true
. Таким образом, используя данное свойство, мы можем контроллировать доступ для тех, кто подтвердил и кто не подтвердил регистрацию.