Partilhar via


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 MVC 5 ASP.NET com confirmação de email e redefinição de senha usando o sistema de associação ASP.NET Identity.

Para obter uma versão atualizada deste tutorial que usa o .NET Core, consulte Confirmação de 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 o Visual Studio 2013. Instale o Visual Studio 2013 Atualização 3 ou superior.

Observação

Aviso: você deve instalar o 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. O Web Forms também oferece suporte ao ASP.NET Identity, para que você possa seguir etapas semelhantes em um aplicativo de formulários da Web.
    Captura de tela que mostra a página do novo projeto A S P dot Net. O modelo MVC é selecionado e Contas de usuário individuais é realçado.

  2. Deixe a autenticação padrão como Contas de usuário individuais. Se você quiser hospedar o aplicativo no Azure, deixe a caixa de seleção marcada. Mais adiante 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 e-mail é com o atributo [EmailAddress].

  5. No Gerenciador de Servidores, 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 de Rede do ASP no Gerenciador de Servidores.

  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 de Usuários de Rede do ASP. A coluna Email confirmado rotulada como False é realçada.
    Neste ponto, o e-mail não foi confirmado.

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

Confirmação por email

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

Geralmente, você deseja impedir que novos usuários publiquem dados em seu site antes que eles sejam confirmados por e-mail, mensagem de texto SMS ou outro mecanismo. Nas seções abaixo, ativaremos a confirmação por e-mail e modificaremos o código para impedir que usuários recém-registrados façam login até que seu e-mail seja confirmado.

Conecte o SendGrid

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

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

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

    Install-Package SendGrid
    
  2. Vá para a página de inscrição do Azure SendGrid e registre-se para obter uma conta gratuita do SendGrid. Configure o SendGrid adicionando um código semelhante ao seguinte no 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 as 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. Consulte Práticas recomendadas para implantar senhas e outros dados confidenciais no ASP.NET e no Azure.

Habilitar confirmação por e-mail 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 faltando. )

@{
    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ê está logado.

Captura de tela que mostra a página inicial de login do My A S P dot NET.

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

Exigir confirmação por e-mail antes do login

Atualmente, uma vez que um usuário preenche o formulário de registro, ele está logado. Geralmente, você deseja confirmar o e-mail antes de fazer login. Na seção abaixo, modificaremos o código para exigir que os novos usuários tenham um e-mail confirmado antes de serem conectados (autenticados). Atualize o HttpPost Register método com as seguintes alterações destacadas:

//
// 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 login até confirmar seu endereço de e-mail. Depois de confirmar seu endereço de e-mail, clique no link Contato.

Recuperação/redefinição de senha

Remova os caracteres de comentário do HttpPost ForgotPassword método de 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 ForgotPassword ActionLink 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 Login agora mostrará um link para redefinir a senha.

Depois que um usuário cria uma nova conta local, ele recebe um link de confirmação por e-mail que deve usar antes de fazer logon. Se o usuário excluir acidentalmente o e-mail de confirmação ou o e-mail 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 de login para reenviar a senha se a conta do 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 sociais e locais

Você pode combinar contas locais e sociais clicando em seu link de email. Na sequência RickAndMSFT@gmail.com a seguir, ele é criado 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 S P dot Net Log In. O exemplo Usuário I D é realçado.

Clique no link Gerenciar. Observe os logins externos: 0 associados a esta conta.

Captura de tela que mostra a página My A S P dot Net Gerenciar sua conta. Ao lado da linha Logons externos, 0 e um link Gerenciar são destacados.

Clique no link para outro serviço de login 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 seus usuários adicionem contas locais caso o serviço de autenticação de login social esteja inativo ou mais provavelmente eles tenham perdido o acesso à conta social.

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

Captura de tela que mostra a página My A S P dot Net Gerenciar sua conta. As linhas Escolher uma senha e Logons externos sã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 S P dot Net Create Local Login. Uma senha de amostra é inserida nos campos de texto Nova senha e Confirmar nova senha.

Confirmação por e-mail com mais detalhes

Meu tutorial Confirmação de conta e recuperação de senha com ASP.NET Identity aborda este tópico com mais detalhes.

Depurando o aplicativo

Se você não receber um e-mail contendo o link:

  • Verifique sua pasta de lixo eletrônico ou spam.
  • Faça login na sua conta do SendGrid e clique no link Atividade de e-mail.

Para testar o link de verificação sem e-mail, 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