로그인, 전자 메일 확인 및 암호 재설정 기능이 있는 보안 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 이상을 설치해야 합니다.
새 ASP.NET 웹 프로젝트를 만들고 MVC 템플릿을 선택합니다. Web Forms는 ASP.NET ID도 지원하므로 웹 양식 앱에서 유사한 단계를 수행할 수 있습니다.
기본 인증을 개별 사용자 계정으로 둡니다. Azure에서 앱을 호스트하려면 확인란을 선택한 상태로 둡니다. 자습서의 뒷부분에서 Azure에 배포합니다. Azure 계정을 무료로 열 수 있습니다.
SSL을 사용하도록 프로젝트를 설정합니다.
앱을 실행하고 등록 링크를 클릭하고 사용자를 등록합니다. 이 시점에서 전자 메일의 유일한 유효성 검사는 [EmailAddress] 특성을 사용하는 것입니다.
서버 탐색기에서 데이터 연결\DefaultConnection\Tables\AspNetUsers로 이동하여 마우스 오른쪽 단추를 클릭하고 테이블 정의 열기를 선택합니다.
다음 이미지는 스키마를
AspNetUsers
보여줍니다.AspNetUsers 테이블을 마우스 오른쪽 단추로 클릭하고 테이블 데이터 표시를 선택합니다.
이 시점에서 이메일이 확인되지 않았습니다.행을 클릭하고 삭제를 선택합니다. 다음 단계에서 이 전자 메일을 다시 추가하고 확인 이메일을 보냅니다.
이메일 확인
새 사용자 등록의 전자 메일을 확인하여 다른 사람을 가장하지 않는지 확인하는 것이 좋습니다(즉, 다른 사람의 전자 메일에 등록되지 않음). 토론 포럼 "bob@example.com"
"joe@contoso.com"
이 있다고 가정해 보겠습니다. 전자 메일 확인 "joe@contoso.com"
이 없으면 앱에서 원치 않는 전자 메일을 받을 수 있습니다. Bob이 실수로 등록되어 "bib@example.com"
눈치채지 못했다고 가정해 보겠습니다. 앱에 올바른 전자 메일이 없기 때문에 암호 복구를 사용할 수 없습니다. 전자 메일 확인은 봇으로부터 제한된 보호만 제공하고 결정된 스패머로부터 보호를 제공하지 않으며 등록하는 데 사용할 수 있는 많은 작업 이메일 별칭이 있습니다.
일반적으로 새 사용자가 전자 메일, SMS 문자 메시지 또는 다른 메커니즘으로 확인되기 전에 웹 사이트에 데이터를 게시하지 못하도록 방지하려고 합니다. 아래 섹션에서는 전자 메일 확인을 사용하도록 설정하고 새로 등록된 사용자가 전자 메일이 확인될 때까지 로그인하지 못하도록 코드를 수정합니다.
SendGrid 연결
이 섹션의 지침은 최신이 아닙니다. 업데이트된 지침은 SendGrid 전자 메일 공급자 구성을 참조하세요.
이 자습서에서는 SendGrid를 통해 전자 메일 알림을 추가하는 방법만 보여 주지만 SMTP 및 기타 메커니즘을 사용하여 전자 메일을 보낼 수 있습니다(추가 리소스 참조).
패키지 관리자 콘솔에서 다음 명령을 입력합니다.
Install-Package SendGrid
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>
앱을 실행하고 등록 링크를 클릭합니다. 등록 양식을 제출하면 로그인됩니다.
전자 메일 계정을 확인하고 링크를 클릭하여 전자 메일을 확인합니다.
로그인하기 전에 전자 메일 확인 필요
현재 사용자가 등록 양식을 완료하면 로그인됩니다. 일반적으로 로그인하기 전에 해당 전자 메일을 확인하려고 합니다. 아래 섹션에서는 새 사용자가 로그인(인증)되기 전에 확인된 전자 메일을 갖도록 코드를 수정합니다. 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 는 먼저 로컬 로그인으로 만들어지지만 먼저 소셜 로그인으로 계정을 만든 다음 로컬 로그인을 추가할 수 있습니다.
관리 링크를 클릭합니다. 외부 로그인: 이 계정과 연결된 0을 확인합니다.
다른 로그인 서비스에 대한 링크를 클릭하고 앱 요청을 수락합니다. 두 계정이 결합되었으므로 두 계정 중 하나를 사용하여 로그온할 수 있습니다. 인증 서비스의 소셜 로그가 다운되거나 소셜 계정에 대한 액세스 권한이 손실된 경우 사용자가 로컬 계정을 추가하도록 할 수 있습니다.
다음 이미지에서 Tom은 소셜 로그인입니다(외부 로그인에서 볼 수 있습니다. 페이지에 표시된 1 ).
암호 선택을 클릭하면 동일한 계정과 연결된 로컬 로그온을 추가할 수 있습니다.
보다 심층적인 전자 메일 확인
ASP.NET ID를 사용한 내 자습서 계정 확인 및 암호 복구는 이 항목에 자세히 설명되어 있습니다.
앱 디버깅
링크가 포함된 전자 메일을 받지 못하는 경우:
- 정크 또는 스팸 폴더를 확인합니다.
- SendGrid 계정에 로그인하고 전자 메일 활동 링크를 클릭합니다.
이메일 없이 확인 링크를 테스트하려면 완료된 샘플을 다운로드합니다. 확인 링크 및 확인 코드가 페이지에 표시됩니다.
추가 리소스
- ASP.NET ID 권장 리소스에 대한 링크
- ASP.NET ID 를 사용한 계정 확인 및 암호 복구는 암호 복구 및 계정 확인에 대해 자세히 설명합니다.
- 페이스 북과 MVC 5 응용 프로그램, 트위터, 링크드 인과 구글 OAuth2 로그온 이 자습서는 페이스 북과 구글 OAuth 2 권한 부여와 ASP.NET MVC 5 응용 프로그램을 작성하는 방법을 보여줍니다. 또한 ID 데이터베이스에 추가 데이터를 추가하는 방법도 보여줍니다.
- Membership, OAuth 및 SQL Database를 사용하여 보안 ASP.NET MVC 앱을 Azure에 배포합니다. 이 자습서에서는 Azure 배포, 역할로 앱을 보호하는 방법, 멤버 자격 API를 사용하여 사용자 및 역할을 추가하는 방법 및 추가 보안 기능을 추가합니다.
- OAuth 2용 Google 앱 만들기 및 프로젝트에 앱 연결
- Facebook에서 앱 만들기 및 프로젝트에 앱 연결
- 프로젝트에서 SSL 설정