Criar um aplicativo Web seguro do ASP.NET MVC 5 com logon, confirmação por email e redefinição de senha (C#)

por Rick Anderson

Este tutorial mostra como criar um aplicativo Web ASP.NET MVC 5 com confirmação de email e redefinição de senha usando o sistema de associação de identidade do ASP.NET.

Para obter uma versão atualizada deste tutorial que usa o .NET Core, confira Confirmação da conta e recuperação de senha no ASP.NET Core.

Criar um aplicativo MVC ASP.NET

Comece instalando e executando o Visual Studio Express 2013 para Web ou Visual Studio 2013. Instale Visual Studio 2013 Atualização 3 ou superior.

Observação

Aviso: você deve instalar Visual Studio 2013 Atualização 3 ou superior para concluir este tutorial.

  1. Crie um novo projeto Web ASP.NET e selecione o modelo MVC. Web Forms também dá suporte ao ASP.NET Identity, para que você possa seguir etapas semelhantes em um aplicativo web forms.
    Captura de tela que mostra a página Novo Projeto de Net de Ponto SP. O modelo M V C está selecionado e Contas de Usuário Individuais está realçada.

  2. Deixe a autenticação padrão como Contas de Usuário Individuais. Se você quiser hospedar o aplicativo no Azure, deixe a caixa marcar marcada. Posteriormente no tutorial, implantaremos no Azure. Você pode abrir uma conta do Azure gratuitamente.

  3. Defina o projeto para usar SSL.

  4. Execute o aplicativo, clique no link Registrar e registre um usuário. Neste ponto, a única validação no email é com o atributo [EmailAddress] .

  5. Em Servidor Explorer, navegue até Conexões de Dados\DefaultConnection\Tables\AspNetUsers, clique com o botão direito do mouse e selecione Abrir definição de tabela.

    A imagem a seguir mostra o AspNetUsers esquema:

    Captura de tela que mostra a guia Arquivo de Script de Usuários do AZure Net no servidor Explorer.

  6. Clique com o botão direito do mouse na tabela AspNetUsers e selecione Mostrar Dados da Tabela.
    Captura de tela que mostra o esquema usuários do ASP Net. A coluna Email Confirmed rotulada como False está realçada.
    Neste ponto, o email não foi confirmado.

  7. Clique na linha e selecione excluir. Você adicionará esse email novamente na próxima etapa e enviará um email de confirmação.

confirmação Email

É uma prática recomendada confirmar o email de um novo registro de usuário para verificar se ele não está se passando por outra pessoa (ou seja, eles não se registraram com o email de outra pessoa). Suponha que você teve um fórum de discussão, você gostaria de impedir "bob@example.com" de se registrar como "joe@contoso.com". Sem confirmação por email, "joe@contoso.com" o pode receber emails indesejados do seu aplicativo. Suponha que Bob se registrou acidentalmente como "bib@example.com" e não tivesse notado, ele não seria capaz de usar a recuperação de senha porque o aplicativo não tem seu email correto. Email confirmação fornece apenas proteção limitada contra bots e não fornece proteção contra spammers determinados, eles têm muitos aliases de email de trabalho que podem usar para se registrar.

Geralmente, você deseja impedir que novos usuários postem dados em seu site antes que eles sejam confirmados por email, uma mensagem de texto SMS ou outro mecanismo. Nas seções abaixo, habilitaremos a confirmação por email e modificaremos o código para impedir que usuários recém-registrados entrem até que seus emails sejam confirmados.

Conectar o SendGrid

As instruções nesta seção não são atuais. Consulte Configurar o provedor de email do SendGrid para obter instruções atualizadas.

Embora este tutorial mostre apenas como adicionar notificação por email por meio do SendGrid, você pode enviar emails usando SMTP e outros mecanismos (consulte recursos adicionais).

  1. No Console do Gerenciador de Pacotes, digite o seguinte comando:

    Install-Package SendGrid
    
  2. Acesse a página de inscrição do Azure SendGrid e registre-se para obter uma conta gratuita do SendGrid. Configure o SendGrid adicionando código semelhante ao seguinte em App_Start/IdentityConfig.cs:

    public class EmailService : IIdentityMessageService
    {
       public async Task SendAsync(IdentityMessage message)
       {
          await configSendGridasync(message);
       }
    
       // Use NuGet to install SendGrid (Basic C# client lib) 
       private async Task configSendGridasync(IdentityMessage message)
       {
          var myMessage = new SendGridMessage();
          myMessage.AddTo(message.Destination);
          myMessage.From = new System.Net.Mail.MailAddress(
                              "Joe@contoso.com", "Joe S.");
          myMessage.Subject = message.Subject;
          myMessage.Text = message.Body;
          myMessage.Html = message.Body;
    
          var credentials = new NetworkCredential(
                     ConfigurationManager.AppSettings["mailAccount"],
                     ConfigurationManager.AppSettings["mailPassword"]
                     );
    
          // Create a Web transport for sending email.
          var transportWeb = new Web(credentials);
    
          // Send the email.
          if (transportWeb != null)
          {
             await transportWeb.DeliverAsync(myMessage);
          }
          else
          {
             Trace.TraceError("Failed to create Web transport.");
             await Task.FromResult(0);
          }
       }
    }
    

Você precisará adicionar os seguintes inclusões:

using SendGrid;
using System.Net;
using System.Configuration;
using System.Diagnostics;

Para manter este exemplo simples, armazenaremos as configurações do aplicativo no arquivo web.config :

</connectionStrings>
   <appSettings>
      <add key="webpages:Version" value="3.0.0.0" />
      <!-- Markup removed for clarity. -->
      
      <add key="mailAccount" value="xyz" />
      <add key="mailPassword" value="password" />
   </appSettings>
  <system.web>

Aviso

Segurança – Nunca armazene dados confidenciais em seu código-fonte. A conta e as credenciais são armazenadas no appSetting. No Azure, você pode armazenar com segurança esses valores na guia Configurar no portal do Azure. Confira Práticas recomendadas para implantar senhas e outros dados confidenciais no ASP.NET e no Azure.

Habilitar a confirmação de email no controlador de conta

//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);

            string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
            var callbackUrl = Url.Action("ConfirmEmail", "Account", 
               new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
            await UserManager.SendEmailAsync(user.Id, 
               "Confirm your account", "Please confirm your account by clicking <a href=\"" 
               + callbackUrl + "\">here</a>");

            return RedirectToAction("Index", "Home");
        }
        AddErrors(result);
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Verifique se o arquivo Views\Account\ConfirmEmail.cshtml tem a sintaxe razor correta. ( O caractere @ na primeira linha pode estar ausente. )

@{
    ViewBag.Title = "Confirm Email";
}

<h2>@ViewBag.Title.</h2>
<div>
    <p>
        Thank you for confirming your email. Please @Html.ActionLink("Click here to Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })
    </p>
</div>

Execute o aplicativo e clique no link Registrar. Depois de enviar o formulário de registro, você estará conectado.

Captura de tela que mostra a página Inicial do My A SP dot NET Log In.

Verifique sua conta de email e clique no link para confirmar seu email.

Exigir confirmação de email antes de fazer logon

Atualmente, depois que um usuário conclui o formulário de registro, ele é conectado. Geralmente, você deseja confirmar seus emails antes de fazer logon neles. Na seção abaixo, modificaremos o código para exigir que novos usuários tenham um email confirmado antes de serem conectados (autenticados). Atualize o HttpPost Register método com as seguintes alterações realçadas:

//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
   if (ModelState.IsValid)
   {
      var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
      var result = await UserManager.CreateAsync(user, model.Password);
      if (result.Succeeded)
      {
         //  Comment the following line to prevent log in until the user is confirmed.
         //  await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);

         string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
         var callbackUrl = Url.Action("ConfirmEmail", "Account",
            new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
         await UserManager.SendEmailAsync(user.Id, "Confirm your account",
            "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");

         // Uncomment to debug locally 
         // TempData["ViewBagLink"] = callbackUrl;

         ViewBag.Message = "Check your email and confirm your account, you must be confirmed "
                         + "before you can log in.";

         return View("Info");
         //return RedirectToAction("Index", "Home");
      }
      AddErrors(result);
   }

   // If we got this far, something failed, redisplay form
   return View(model);
}

Ao comentar o SignInAsync método , o usuário não será conectado pelo registro. A TempData["ViewBagLink"] = callbackUrl; linha pode ser usada para depurar o aplicativo e testar o registro sem enviar email. ViewBag.Message é usado para exibir as instruções de confirmação. O exemplo de download contém código para testar a confirmação de email sem configurar o email e também pode ser usado para depurar o aplicativo.

Crie um Views\Shared\Info.cshtml arquivo e adicione a seguinte marcação razor:

@{
   ViewBag.Title = "Info";
}
<h2>@ViewBag.Title.</h2>
<h3>@ViewBag.Message</h3>

Adicione o atributo Authorize ao Contact método de ação do controlador Home. Você pode clicar no link Contato para verificar se os usuários anônimos não têm acesso e se os usuários autenticados têm acesso.

[Authorize]
public ActionResult Contact()
{
   ViewBag.Message = "Your contact page.";

   return View();
}

Você também deve atualizar o método de HttpPost Login ação:

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    // Require the user to have a confirmed email before they can log on.
    var user = await UserManager.FindByNameAsync(model.Email);
    if (user != null)
    {
       if (!await UserManager.IsEmailConfirmedAsync(user.Id))
       {
          ViewBag.errorMessage = "You must have a confirmed email to log on.";
          return View("Error");
       }
    }

    // This doesn't count login failures towards account lockout
    // To enable password failures to trigger account lockout, change to shouldLockout: true
    var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}

Atualize a exibição Views\Shared\Error.cshtml para exibir a mensagem de erro:

@model System.Web.Mvc.HandleErrorInfo

@{
    ViewBag.Title = "Error";
}

<h1 class="text-danger">Error.</h1>
@{
   if (String.IsNullOrEmpty(ViewBag.errorMessage))
   {
      <h2 class="text-danger">An error occurred while processing your request.</h2>
   }
   else
   {
      <h2 class="text-danger">@ViewBag.errorMessage</h2>
   }
}

Exclua todas as contas na tabela AspNetUsers que contenham o alias de email com o qual você deseja testar. Execute o aplicativo e verifique se você não pode fazer logon até confirmar seu endereço de email. Depois de confirmar seu endereço de email, clique no link Contato .

Recuperação/redefinição de senha

Remova os caracteres de comentário do método de HttpPost ForgotPassword ação no controlador de conta:

//
// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindByNameAsync(model.Email);
        if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
        {
            // Don't reveal that the user does not exist or is not confirmed
            return View("ForgotPasswordConfirmation");
        }

        string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
        var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
        await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking <a href=\"" + callbackUrl + "\">here</a>");
        return RedirectToAction("ForgotPasswordConfirmation", "Account");
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Remova os caracteres de comentário do ForgotPasswordActionLink no arquivo de exibição razor Views\Account\Login.cshtml :

@using MvcPWy.Models
@model LoginViewModel
@{
   ViewBag.Title = "Log in";
}

<h2>@ViewBag.Title.</h2>
<div class="row">
   <div class="col-md-8">
      <section id="loginForm">
         @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
         {
            @Html.AntiForgeryToken()
            <h4>Use a local account to log in.</h4>
            <hr />
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })
            <div class="form-group">
               @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
               <div class="col-md-10">
                  @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
                  @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
               </div>
            </div>
            <div class="form-group">
               @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
               <div class="col-md-10">
                  @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                  @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
               </div>
            </div>
            <div class="form-group">
               <div class="col-md-offset-2 col-md-10">
                  <div class="checkbox">
                     @Html.CheckBoxFor(m => m.RememberMe)
                     @Html.LabelFor(m => m.RememberMe)
                  </div>
               </div>
            </div>
            <div class="form-group">
               <div class="col-md-offset-2 col-md-10">
                  <input type="submit" value="Log in" class="btn btn-default" />
               </div>
            </div>
            <p>
               @Html.ActionLink("Register as a new user", "Register")
            </p>
            @* Enable this once you have account confirmation enabled for password reset functionality *@
            <p>
               @Html.ActionLink("Forgot your password?", "ForgotPassword")
            </p>
         }
      </section>
   </div>
   <div class="col-md-4">
      <section id="socialLoginForm">
         @Html.Partial("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl })
      </section>
   </div>
</div>

@section Scripts {
   @Scripts.Render("~/bundles/jqueryval")
}

A página Fazer logon agora mostrará um link para redefinir a senha.

Depois que um usuário cria uma nova conta local, ele é enviado por email um link de confirmação que ele precisa usar antes de fazer logon. Se o usuário excluir acidentalmente o email de confirmação ou o email nunca chegar, ele precisará do link de confirmação enviado novamente. As alterações de código a seguir mostram como habilitar isso.

Adicione o seguinte método auxiliar à parte inferior do arquivo Controllers\AccountController.cs :

private async Task<string> SendEmailConfirmationTokenAsync(string userID, string subject)
{
   string code = await UserManager.GenerateEmailConfirmationTokenAsync(userID);
   var callbackUrl = Url.Action("ConfirmEmail", "Account",
      new { userId = userID, code = code }, protocol: Request.Url.Scheme);
   await UserManager.SendEmailAsync(userID, subject,
      "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");

   return callbackUrl;
}

Atualize o método Register para usar o novo auxiliar:

//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
   if (ModelState.IsValid)
   {
      var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
      var result = await UserManager.CreateAsync(user, model.Password);
      if (result.Succeeded)
      {
         //  Comment the following line to prevent log in until the user is confirmed.
         //  await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);

         string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account");

         ViewBag.Message = "Check your email and confirm your account, you must be confirmed "
                         + "before you can log in.";

         return View("Info");
         //return RedirectToAction("Index", "Home");
      }
      AddErrors(result);
   }

   // If we got this far, something failed, redisplay form
   return View(model);
}

Atualize o método Login para reenviar a senha se a conta de usuário não tiver sido confirmada:

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
   if (!ModelState.IsValid)
   {
      return View(model);
   }

   // Require the user to have a confirmed email before they can log on.
  // var user = await UserManager.FindByNameAsync(model.Email);
   var user =  UserManager.Find(model.Email, model.Password);
   if (user != null)
   {
      if (!await UserManager.IsEmailConfirmedAsync(user.Id))
      {
         string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account-Resend");

          // Uncomment to debug locally  
          // ViewBag.Link = callbackUrl;
         ViewBag.errorMessage = "You must have a confirmed email to log on. "
                              + "The confirmation token has been resent to your email account.";
         return View("Error");
      }
   }

   // This doesn't count login failures towards account lockout
   // To enable password failures to trigger account lockout, change to shouldLockout: true
   var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
   switch (result)
   {
      case SignInStatus.Success:
         return RedirectToLocal(returnUrl);
      case SignInStatus.LockedOut:
         return View("Lockout");
      case SignInStatus.RequiresVerification:
         return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
      case SignInStatus.Failure:
      default:
         ModelState.AddModelError("", "Invalid login attempt.");
         return View(model);
   }
}

Combinar contas de logon locais e sociais

Você pode combinar contas locais e sociais clicando em seu link de email. Na sequência RickAndMSFT@gmail.com a seguir é criada primeiro como um logon local, mas você pode criar a conta como um logon social primeiro e, em seguida, adicionar um logon local.

Captura de tela que mostra a página Inicial do My A SP dot Net Log In. A ID de usuário de exemplo está realçada.

Clique no link Gerenciar . Observe os Logons Externos: 0 associados a essa conta.

Captura de tela que mostra a página My A SP dot Net Manage your account. Ao lado da linha Logons Externos, 0 e um link Gerenciar estão realçados.

Clique no link para outro serviço de logon e aceite as solicitações do aplicativo. As duas contas foram combinadas, você poderá fazer logon com qualquer uma das contas. Talvez você queira que os usuários adicionem contas locais caso o log social deles no serviço de autenticação esteja inativo ou, provavelmente, eles tenham perdido o acesso à conta social.

Na imagem a seguir, Tom é um logon social (que você pode ver nos Logons Externos: 1 mostrado na página).

Captura de tela que mostra a página My A SP dot Net Manage your account. As linhas Escolher uma senha e Logons Externos estão realçadas.

Clicar em Escolher uma senha permite adicionar um logon local associado à mesma conta.

Captura de tela que mostra a página My A SP dot Net Create Local Login. Uma senha de exemplo é inserida nos campos Nova senha e Confirmar novo texto de senha.

Email confirmação mais detalhada

Meu tutorial Confirmação da Conta e Recuperação de Senha com ASP.NET Identidade entra neste tópico com mais detalhes.

Depurando o aplicativo

Se você não receber um email contendo o link:

  • Verifique sua pasta de lixo eletrônico ou spam.
  • Faça logon em sua conta do SendGrid e clique no link Atividade do Email.

Para testar o link de verificação sem email, baixe o exemplo concluído. O link de confirmação e os códigos de confirmação serão exibidos na página.

Recursos adicionais