Creare un'app Web ASP.NET MVC 5 sicura con accesso, messaggi di posta elettronica di conferma e reimpostazione della password (C#)

di Rick Anderson

Questa esercitazione illustra come creare un'app Web ASP.NET MVC 5 con la conferma tramite posta elettronica e la reimpostazione della password usando il sistema di appartenenza ASP.NET Identity.

Per una versione aggiornata di questa esercitazione che usa .NET Core, vedere Conferma dell'account e ripristino della password in ASP.NET Core.

Creare un'app MVC ASP.NET

Per iniziare, installare ed eseguire Visual Studio Express 2013 per Web o Visual Studio 2013. Installare Visual Studio 2013 Update 3 o versione successiva.

Nota

Avviso: per completare questa esercitazione è necessario installare Visual Studio 2013 Update 3 o versione successiva.

  1. Creare un nuovo progetto Web ASP.NET e selezionare il modello MVC. Web Form supporta anche ASP.NET Identity, quindi è possibile seguire passaggi simili in un'app Web Form.
    Screenshot che mostra la pagina New A S P dot Net Project .Screenshot che mostra la pagina New A S P dot Net Project . Il modello M V C è selezionato e l'opzione Account utente singoli è evidenziata.

  2. Lasciare l'autenticazione predefinita come Account utente singoli. Se si vuole ospitare l'app in Azure, lasciare selezionata la casella di controllo. Più avanti nell'esercitazione verrà distribuita in Azure. È possibile aprire gratuitamente un account Azure.

  3. Impostare il progetto per l'uso di SSL.

  4. Eseguire l'app, fare clic sul collegamento Registra e registrare un utente. A questo punto, l'unica convalida sul messaggio di posta elettronica è con l'attributo [EmailAddress].

  5. In Esplora server passare a Connessioni dati\DefaultConnection\Tables\AspNetUsers, fare clic con il pulsante destro del mouse e selezionare Apri definizione tabella.

    L'immagine seguente mostra lo AspNetUsers schema:

    Screenshot che mostra la scheda A S P Net Users Script File in Esplora server.

  6. Fare clic con il pulsante destro del mouse sulla tabella AspNetUsers e selezionare Mostra dati tabella.
    Screenshot che mostra lo schema A S P Net Users. La colonna Email Confirmed con etichetta False è evidenziata.
    A questo punto il messaggio di posta elettronica non è stato confermato.

  7. Fare clic sulla riga e selezionare Elimina. Questo messaggio di posta elettronica verrà aggiunto di nuovo nel passaggio successivo e verrà inviato un messaggio di posta elettronica di conferma.

Conferma tramite posta elettronica

È consigliabile confermare l'indirizzo di posta elettronica di una nuova registrazione utente per verificare che non stiano rappresentando un altro utente( ovvero non sono stati registrati con un messaggio di posta elettronica di un altro utente). Si supponga di avere avuto un forum di discussione, si vuole impedire "bob@example.com" la registrazione come "joe@contoso.com". Senza la conferma tramite posta elettronica, "joe@contoso.com" potrebbe ricevere un messaggio di posta elettronica indesiderato dall'app. Si supponga che Bob sia stato registrato accidentalmente come "bib@example.com" e non l'avesse notato, non sarebbe in grado di usare il ripristino della password perché l'app non ha il messaggio di posta elettronica corretto. La conferma tramite posta elettronica offre solo una protezione limitata dai bot e non fornisce protezione da spammer determinati, ma hanno molti alias di posta elettronica funzionanti che possono usare per la registrazione.

In genere si vuole impedire ai nuovi utenti di pubblicare dati nel sito Web prima che siano stati confermati tramite posta elettronica, sms o un altro meccanismo. Nelle sezioni seguenti si abiliterà la conferma tramite posta elettronica e si modificherà il codice per impedire agli utenti appena registrati di accedere fino alla conferma del messaggio di posta elettronica.

Associare SendGrid

Le istruzioni contenute in questa sezione non sono aggiornate. Per istruzioni aggiornate, vedere Configurare il provider di posta elettronica SendGrid.

Anche se questa esercitazione illustra solo come aggiungere notifiche tramite SendGrid, è possibile inviare messaggi di posta elettronica usando SMTP e altri meccanismi (vedere risorse aggiuntive).

  1. Nella Console di Gestione pacchetti immettere il comando seguente:

    Install-Package SendGrid
    
  2. Passare alla pagina di iscrizione di Azure SendGrid e registrarsi per ottenere un account SendGrid gratuito. Configurare SendGrid aggiungendo codice simile al seguente in 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);
          }
       }
    }
    

È necessario aggiungere quanto segue:

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

Per semplificare questo esempio, le impostazioni dell'app verranno archiviate nel file 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>

Avviso

Sicurezza: non archiviare mai i dati sensibili nel codice sorgente. L'account e le credenziali vengono archiviati in appSetting. In Azure è possibile archiviare questi valori in modo sicuro nella scheda Configura nella portale di Azure. Vedere Procedure consigliate per la distribuzione di password e altri dati sensibili in ASP.NET e Azure.

Abilitare la conferma tramite posta elettronica nel controller dell'account

//
// 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);
}

Verificare che il file Views\Account\ConfirmEmail.cshtml abbia la sintassi razor corretta. ( Il carattere @ nella prima riga potrebbe non essere presente. )

@{
    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>

Eseguire l'app e fare clic sul collegamento Registra. Dopo aver inviato il modulo di registrazione, si è connessi.

Screenshot che mostra la home page My A S P dot NET Log In.

Controllare l'account di posta elettronica e fare clic sul collegamento per confermare il messaggio di posta elettronica.

Richiedi conferma tramite posta elettronica prima dell'accesso

Attualmente dopo che un utente ha completato il modulo di registrazione, viene eseguito l'accesso. In genere si vuole confermare il messaggio di posta elettronica prima di accedervi. Nella sezione seguente il codice verrà modificato in modo da richiedere ai nuovi utenti di avere un messaggio di posta elettronica confermato prima dell'accesso (autenticato). Aggiornare il HttpPost Register metodo con le modifiche evidenziate seguenti:

//
// 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);
}

Commentando il SignInAsync metodo, l'utente non verrà connesso dalla registrazione. La TempData["ViewBagLink"] = callbackUrl; riga può essere usata per eseguire il debug dell'app e testare la registrazione senza inviare messaggi di posta elettronica. ViewBag.Message viene usato per visualizzare le istruzioni di conferma. L'esempio di download contiene il codice per testare la conferma tramite posta elettronica senza configurare il messaggio di posta elettronica e può essere usato anche per eseguire il debug dell'applicazione.

Creare un Views\Shared\Info.cshtml file e aggiungere il markup razor seguente:

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

Aggiungere l'attributo Authorize al Contact metodo action del controller Home. È possibile fare clic sul collegamento Contatto per verificare che gli utenti anonimi non abbiano accesso e che gli utenti autenticati abbiano accesso.

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

   return View();
}

È anche necessario aggiornare il HttpPost Login metodo di azione:

//
// 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);
    }
}

Aggiornare la visualizzazione Views\Shared\Error.cshtml per visualizzare il messaggio di errore:

@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>
   }
}

Eliminare tutti gli account nella tabella AspNetUsers che contengono l'alias di posta elettronica con cui si vuole eseguire il test. Eseguire l'app e verificare che non sia possibile accedere fino a quando non è stato confermato l'indirizzo di posta elettronica. Dopo aver confermato l'indirizzo di posta elettronica, fare clic sul collegamento Contatto .

Ripristino/reimpostazione della password

Rimuovere i caratteri di commento dal HttpPost ForgotPassword metodo di azione nel controller dell'account:

//
// 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);
}

Rimuovere i caratteri di ForgotPassword commento da ActionLink nel file di visualizzazione 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")
}

Nella pagina Accedi verrà ora visualizzato un collegamento per reimpostare la password.

Dopo che un utente crea un nuovo account locale, viene inviato tramite posta elettronica un collegamento di conferma da usare prima di poter accedere. Se l'utente elimina accidentalmente il messaggio di posta elettronica di conferma o il messaggio di posta elettronica non arriva mai, sarà necessario il collegamento di conferma inviato di nuovo. Le modifiche al codice seguenti illustrano come abilitare questa opzione.

Aggiungere il metodo helper seguente alla fine del file 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;
}

Aggiornare il metodo Register per usare il nuovo helper:

//
// 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);
}

Aggiornare il metodo Login per inviare nuovamente la password se l'account utente non è stato confermato:

//
// 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);
   }
}

Combinare account di accesso di social network e locali

È possibile combinare account locali e social facendo clic sul collegamento di posta elettronica. Nella sequenza RickAndMSFT@gmail.com seguente viene prima creato come account di accesso locale, ma è possibile creare prima l'account come accesso di social networking e quindi aggiungere un account di accesso locale.

Screenshot che mostra la home page My A S P dot Net Log In. L'esempio user I D è evidenziato.

Fare clic sul collegamento Gestisci . Si notino gli account di accesso esterni: 0 associati a questo account.

Screenshot che mostra la pagina My A S P dot Net Manage your account .Screenshot che mostra la pagina My A S P dot Net Manage your account .Screenshot that shows the My A S P dot Net Manage your account page. Accanto alla riga Account di accesso esterni, 0 e un collegamento Gestisci è evidenziato.

Fare clic sul collegamento a un altro servizio di accesso e accettare le richieste dell'app. I due account sono stati combinati, sarà possibile accedere con uno dei due account. È possibile che gli utenti aggiungano account locali nel caso in cui il servizio di autenticazione del proprio account di social networking sia inattivo o che abbiano perso l'accesso al proprio account di social networking.

Nell'immagine seguente, Tom è un log di social networking (che è possibile visualizzare dagli account di accesso esterni: 1 mostrato nella pagina).

Screenshot che mostra la pagina My A S P dot Net Manage your account .Screenshot che mostra la pagina My A S P dot Net Manage your account .Screenshot that shows the My A S P dot Net Manage your account page. Le righe Selezionare una password e Account di accesso esterno sono evidenziate.

Facendo clic su Seleziona una password è possibile aggiungere un accesso locale associato allo stesso account.

Screenshot che mostra la pagina My A S P dot Net Create Local Login (My A S P dot Net Create Local Login). Una password di esempio viene immessa nei campi Nuova password e Conferma nuovo testo password.

Conferma tramite posta elettronica in modo più approfondito

L'esercitazione Conferma account e ripristino password con identità ASP.NET illustra questo argomento con altri dettagli.

Debug dell'app

Se non si riceve un messaggio di posta elettronica contenente il collegamento:

  • Controllare la cartella posta indesiderata o posta indesiderata.
  • Accedere all'account SendGrid e fare clic sul collegamento Attività di posta elettronica.

Per testare il collegamento di verifica senza posta elettronica, scaricare l'esempio completato. Il collegamento di conferma e i codici di conferma verranno visualizzati nella pagina.

Risorse aggiuntive