다음을 통해 공유


로그인, 전자 메일 확인 및 암호 재설정 기능이 있는 보안 ASP.NET MVC 5 웹앱 만들기(C#)

작성자: Rick Anderson

이 자습서에서는 ASP.NET ID 멤버 자격 시스템을 사용하여 전자 메일 확인 및 암호 재설정을 사용하여 ASP.NET MVC 5 웹앱을 빌드하는 방법을 보여줍니다.

.NET Core를 사용하는 이 자습서의 업데이트된 버전은 ASP.NET Core의 계정 확인 및 암호 복구를 참조 하세요.

ASP.NET MVC 앱 만들기

먼저 Visual Studio Express 2013 for Web 또는 Visual Studio 2013을 설치하고 실행합니다. Visual Studio 2013 업데이트 3 이상을 설치합니다.

참고 항목

경고: 이 자습서를 완료하려면 Visual Studio 2013 업데이트 3 이상을 설치해야 합니다.

  1. 새 ASP.NET 웹 프로젝트를 만들고 MVC 템플릿을 선택합니다. Web Forms는 ASP.NET ID도 지원하므로 웹 양식 앱에서 유사한 단계를 수행할 수 있습니다.
    새 A S P 점 Net 프로젝트 페이지를 보여 주는 스크린샷. M V C 템플릿이 선택되고 개별 사용자 계정이 강조 표시됩니다.

  2. 기본 인증을 개별 사용자 계정으로 둡니다. Azure에서 앱을 호스트하려면 확인란을 선택한 상태로 둡니다. 자습서의 뒷부분에서 Azure에 배포합니다. Azure 계정을 무료로 열 수 있습니다.

  3. SSL사용하도록 프로젝트를 설정합니다.

  4. 앱을 실행하고 등록 링크를 클릭하고 사용자를 등록합니다. 이 시점에서 전자 메일의 유일한 유효성 검사는 [EmailAddress] 특성을 사용하는 것입니다.

  5. 서버 탐색기에서 데이터 연결\DefaultConnection\Tables\AspNetUsers로 이동하여 마우스 오른쪽 단추를 클릭하고 테이블 정의 열기를 선택합니다.

    다음 이미지는 스키마를 AspNetUsers 보여줍니다.

    서버 탐색기의 A SP Net 사용자 스크립트 파일 탭을 보여 주는 스크린샷

  6. AspNetUsers 테이블을 마우스 오른쪽 단추로 클릭하고 테이블 데이터 표시를 선택합니다.
    A S P Net 사용자 스키마를 보여 주는 스크린샷 False로 레이블이 지정된 전자 메일 확인 열이 강조 표시됩니다.
    이 시점에서 이메일이 확인되지 않았습니다.

  7. 행을 클릭하고 삭제를 선택합니다. 다음 단계에서 이 전자 메일을 다시 추가하고 확인 이메일을 보냅니다.

이메일 확인

새 사용자 등록의 전자 메일을 확인하여 다른 사람을 가장하지 않는지 확인하는 것이 좋습니다(즉, 다른 사람의 전자 메일에 등록되지 않음). 토론 포럼 "bob@example.com" "joe@contoso.com"이 있다고 가정해 보겠습니다. 전자 메일 확인 "joe@contoso.com" 이 없으면 앱에서 원치 않는 전자 메일을 받을 수 있습니다. Bob이 실수로 등록되어 "bib@example.com" 눈치채지 못했다고 가정해 보겠습니다. 앱에 올바른 전자 메일이 없기 때문에 암호 복구를 사용할 수 없습니다. 전자 메일 확인은 봇으로부터 제한된 보호만 제공하고 결정된 스패머로부터 보호를 제공하지 않으며 등록하는 데 사용할 수 있는 많은 작업 이메일 별칭이 있습니다.

일반적으로 새 사용자가 전자 메일, SMS 문자 메시지 또는 다른 메커니즘으로 확인되기 전에 웹 사이트에 데이터를 게시하지 못하도록 방지하려고 합니다. 아래 섹션에서는 전자 메일 확인을 사용하도록 설정하고 새로 등록된 사용자가 전자 메일이 확인될 때까지 로그인하지 못하도록 코드를 수정합니다.

SendGrid 연결

이 섹션의 지침은 최신이 아닙니다. 업데이트된 지침은 SendGrid 전자 메일 공급자 구성을 참조하세요.

이 자습서에서는 SendGrid를 통해 전자 메일 알림을 추가하는 방법만 보여 주지만 SMTP 및 기타 메커니즘을 사용하여 전자 메일을 보낼 수 있습니다(추가 리소스 참조).

  1. 패키지 관리자 콘솔에서 다음 명령을 입력합니다.

    Install-Package SendGrid
    
  2. Azure SendGrid 등록 페이지로 이동하여 체험 SendGrid 계정에 등록합니다. App_Start/IdentityConfig.cs 다음과 유사한 코드를 추가하여 SendGrid를 구성합니다.

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

다음 포함을 추가해야 합니다.

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

이 샘플을 간단하게 유지하기 위해 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>

Warning

보안 - 소스 코드에 중요한 데이터를 저장하지 않습니다. 계정 및 자격 증명은 appSetting에 저장됩니다. Azure에서 이러한 값을 Azure Portal의 구성 탭에 안전하게 저장할 수 있습니다. ASP.NET 및 Azure에 암호 및 기타 중요한 데이터를 배포하는 모범 사례를 참조하세요.

계정 컨트롤러에서 전자 메일 확인 사용

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

Views\Account\ConfirmEmail.cshtml 파일에 올바른 razor 구문이 있는지 확인합니다. (첫 번째 줄의 @ 문자가 누락되었을 수 있습니다. )

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

앱을 실행하고 등록 링크를 클릭합니다. 등록 양식을 제출하면 로그인됩니다.

내 ASP 점 NET 로그인 홈 페이지를 보여 주는 스크린샷.

전자 메일 계정을 확인하고 링크를 클릭하여 전자 메일을 확인합니다.

로그인하기 전에 전자 메일 확인 필요

현재 사용자가 등록 양식을 완료하면 로그인됩니다. 일반적으로 로그인하기 전에 해당 전자 메일을 확인하려고 합니다. 아래 섹션에서는 새 사용자가 로그인(인증)되기 전에 확인된 전자 메일을 갖도록 코드를 수정합니다. HttpPost Register 다음과 같이 강조 표시된 변경 내용으로 메서드를 업데이트합니다.

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

메서드를 주석으로 SignInAsync 처리하면 사용자가 등록에 의해 로그인되지 않습니다. 이 TempData["ViewBagLink"] = callbackUrl; 줄을 사용하여 전자 메일을 보내지 않고도 앱을 디버그하고 등록을 테스트할 수 있습니다. ViewBag.Message 은 확인 지침을 표시하는 데 사용됩니다. 다운로드 샘플에는 전자 메일을 설정하지 않고 전자 메일 확인을 테스트하는 코드가 포함되어 있으며 애플리케이션을 디버그하는 데 사용할 수도 있습니다.

Views\Shared\Info.cshtml 파일을 만들고 다음 Razor 태그를 추가합니다.

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

Home 컨트롤러의 작업 메서드에 Contact Authorize 특성을 추가합니다. 연락처 링크를 클릭하여 익명 사용자에게 액세스 권한이 없고 인증된 사용자에게 액세스 권한이 있는지 확인할 수 있습니다.

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

   return View();
}

작업 메서드도 업데이트 HttpPost Login 해야 합니다.

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

Views\Shared\Error.cshtml 보기를 업데이트하여 오류 메시지를 표시합니다.

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

테스트하려는 전자 메일 별칭이 포함된 AspNetUsers 테이블의 모든 계정을 삭제합니다. 앱을 실행하고 이메일 주소를 확인할 때까지 로그인할 수 없는지 확인합니다. 전자 메일 주소를 확인하면 연락처 링크를 클릭합니다.

암호 복구/재설정

계정 컨트롤러의 HttpPost ForgotPassword 작업 메서드에서 주석 문자를 제거합니다.

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

Views\Account\Login.cshtml razor 뷰 파일의 ActionLink에서 ForgotPassword 주석 문자를 제거합니다.

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

이제 로그인 페이지에 암호를 재설정하는 링크가 표시됩니다.

사용자가 새 로컬 계정을 만들면 로그온하기 전에 사용해야 하는 확인 링크가 전자 메일로 보내집니다. 사용자가 실수로 확인 전자 메일을 삭제하거나 전자 메일이 도착하지 않는 경우 다시 전송된 확인 링크가 필요합니다. 다음 코드 변경 내용은 이를 사용하도록 설정하는 방법을 보여 줍니다.

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

새 도우미를 사용하도록 Register 메서드를 업데이트합니다.

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

사용자 계정이 확인되지 않은 경우 암호를 다시 보내도록 로그인 방법을 업데이트합니다.

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

소셜 및 로컬 로그인 계정 결합

이메일 링크를 클릭하여 로컬 및 소셜 계정을 결합할 수 있습니다. 다음 시퀀스에서 RickAndMSFT@gmail.com 는 먼저 로컬 로그인으로 만들어지지만 먼저 소셜 로그인으로 계정을 만든 다음 로컬 로그인을 추가할 수 있습니다.

내 ASP 점 Net 로그인 홈페이지를 보여 주는 스크린샷 샘플 사용자 I D가 강조 표시되어 있습니다.

관리 링크를 클릭합니다. 외부 로그인: 이 계정과 연결된 0을 확인합니다.

내 ASP 점 Net 계정 관리 페이지를 보여 주는 스크린샷. 외부 로그인 줄 옆에 0과 관리 링크가 강조 표시됩니다.

다른 로그인 서비스에 대한 링크를 클릭하고 앱 요청을 수락합니다. 두 계정이 결합되었으므로 두 계정 중 하나를 사용하여 로그온할 수 있습니다. 인증 서비스의 소셜 로그가 다운되거나 소셜 계정에 대한 액세스 권한이 손실된 경우 사용자가 로컬 계정을 추가하도록 할 수 있습니다.

다음 이미지에서 Tom은 소셜 로그인입니다(외부 로그인에서 볼 수 있습니다. 페이지에 표시된 1 ).

내 ASP 점 Net 계정 관리 페이지를 보여 주는 스크린샷. 암호 선택 및 외부 로그인 줄이 강조 표시됩니다.

암호 선택을 클릭하면 동일한 계정과 연결된 로컬 로그온을 추가할 수 있습니다.

내 ASP 점 Net 로컬 로그인 만들기 페이지를 보여 주는 스크린샷. 새 암호 및 새 암호 확인 텍스트 필드에 샘플 암호가 입력됩니다.

보다 심층적인 전자 메일 확인

ASP.NET ID를 사용한 내 자습서 계정 확인 및 암호 복구는 이 항목에 자세히 설명되어 있습니다.

앱 디버깅

링크가 포함된 전자 메일을 받지 못하는 경우:

  • 정크 또는 스팸 폴더를 확인합니다.
  • SendGrid 계정에 로그인하고 전자 메일 활동 링크를 클릭합니다.

이메일 없이 확인 링크를 테스트하려면 완료된 샘플을 다운로드합니다. 확인 링크 및 확인 코드가 페이지에 표시됩니다.

추가 리소스