인증: 계정 확인 및 비밀번호 복구

등록일시: 2017-03-06 08:00,  수정일시: 2017-08-24 14:41
조회수: 6,412
이 문서는 ASP.NET Core 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 계정 등록에 사용된 이메일 주소를 확인하는 기능과 비밀번호 재설정 기능을 지원하는 ASP.NET Core 응용 프로그램의 구현 방법을 살펴봅니다.
주의

이 페이지의 문서는 버전 1.0.0-rc2를 기준으로 작성되었으며, 아직 버전 1.0.0으로 갱신되지 않았습니다.

본문에서는 계정 등록에 사용된 이메일 주소를 확인하는 기능과 비밀번호 재설정 기능을 지원하는 ASP.NET Core 응용 프로그램의 구현 방법을 살펴봅니다.

새로운 ASP.NET Core 프로젝트 생성하기

노트

본 자습서는 Visual Studio 2015 업데이트 2 및 ASP.NET Core RC2 이상을 필요로 합니다.

  • Visual Studio에서 새로운 프로젝트를 생성합니다 (시작 페이지 또는 파일(File) > 새로 만들기(New) > 프로젝트(Project) 메뉴를 이용해서):

  • 웹 응용 프로그램(Web Application)을 선택하고 인증(Authentication) 방법이 개별 사용자 계정(Individual User Accounts)으로 선택되어 있는지 확인합니다:

응용 프로그램을 실행하고 Register 링크를 클릭해서 새로운 사용자를 등록합니다. 이메일 주소에 반영된 유효성 검사는 [EmailAddress] 어트리뷰트가 적용된 것이 유일합니다. 등록 양식을 작성하고 제출하면 자동으로 응용 프로그램에 로그인 됩니다. 본문에서는 등록에 사용된 이메일 주소의 유효성이 확인될 때까지 새로운 사용자의 로그인이 불가능하도록 이 동작을 변경할 것입니다.

다시 Visual Studio로 돌아와서 SQL Server 개체 탐색기(SSOX, SQL Server Object Explorer)에서 (localdb)MSSQLLocalDB(SQL Server 12)로 이동합니다. 마우스 오른쪽 버튼으로 dbo.AspNetUsers 테이블을 선택한 다음, 데이터 보기(View Data)를 선택합니다:

데이터의 EmailConfirmed 필드에 False 값이 설정되어 있다는 점에 주목하시기 바랍니다.

데이터 로우를 마우스 오른쪽 버튼으로 클릭한 다음, 컨텍스트 메뉴에서 삭제(Delete)를 선택합니다. 본문의 이후 과정 중, 응용 프로그램에서 확인 이메일을 전송할 때 해당 이메일 주소를 다시 사용하려면, 지금 데이터를 삭제하는 것이 훨씬 용이하기 때문입니다.

SSL을 필수로 지정하기

이번 절에서는 Visual Studio 프로젝트가 SSL을 필수로 사용하도록 설정하겠습니다.

Visual Studio에서 SSL 활성화시키기

  • 솔루션 탐색기(Solution Explorer)에서 마우스 오른쪽 버튼으로 프로젝트를 클릭한 다음, 속성(Properties)을 선택합니다.

  • 좌측 패인에서 디버그(Debug) 탭을 선택합니다.

  • SSL 사용(Enable SSL) 항목을 체크합니다.

  • SSL URL을 복사해서 앱 URL(App URL)에 붙여 넣습니다:

  • Startup.cs 파일의 ConfigureServices 메서드에 다음 코드를 추가합니다:
services.Configure<MvcOptions>(options =>
{
    options.Filters.Add(new RequireHttpsAttribute ());
});

그리고 각각의 컨트롤러에 [RequireHttps] 어트리뷰트를 추가합니다. [RequireHttps] 어트리뷰트는 모든 HTTP GET 요청을 HTTPS GET으로 재지정하고, HTTP POST 요청은 거부합니다. 보안적으로 가장 권장되는 방식은 모든 요청에 HTTPS를 사용하는 것입니다.

[RequireHttps]
public class HomeController : Controller
노트

여기까지 내용을 적용한 예제 응용 프로그램을 실제로 테스트해보면 [RequireHttps] 어트리뷰트를 적용했다고 해서 자동으로 HTTP GET 요청이 HTTPS GET 요청으로 재지정되지는 않습니다.

이메일 확인 요구하기

새로운 사용자가 등록되면 이메일의 유효성을 확인해서 다른 사용자를 사칭하지 않았음을 (즉, 다른 사람의 이메일 주소로 등록하지 않았음을) 확인하는 것이 좋습니다. 예를 들어서, 여러분이 토론 포럼을 운영하고 있다면 "yli@example.com"이라는 이메일 주소를 사용하는 사용자가 "nolivetto@contoso.com"이라는 이메일 주소로 등록하는 상황을 원하지는 않을 것입니다. 만약 이메일 주소를 확인하지 않는다면 "nolivetto@contoso.com"의 실제 소유자는 응용 프로그램으로부터 불필요한 이메일을 수신받을 수도 있습니다. 게다가, 사용자가 실수로 "yli"의 오타인 "ylo@example.com"으로 등록된 상태에서 그 사실을 인지하지 못한다면, 응용 프로그램이 올바른 이메일 주소를 알지 못하므로 비밀번호 복원 기능을 활용할 수도 없습니다. 이메일 확인 기능은 봇에 대응할 수 있는 제한된 보호 기능만 제공할 뿐, 등록에 사용 가능한 다수의 실제 이메일 주소를 보유하고 있는 스패머의 공격까지 보호해주지는 못합니다.

일반적으로 새로운 사용자는 확인 이메일을 수신받기 전까지 웹 사이트에 어떠한 데이터도 포스팅할 수 없는 경우가 대부분입니다. 이번 절에서는 이메일 확인 기능을 활성화시키고 코드를 변경해서 새로 등록한 사용자가 이메일 주소의 유효성을 확인 받기 전까지는 로그인이 불가능하도록 만들어보겠습니다.

먼저 확인 이메일을 필수로 요구하도록 ConfigureServices의 구성을 변경합니다:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddApplicationInsightsTelemetry(Configuration);

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>(config =>
        {
            config.SignIn.RequireConfirmedEmail = true;
        })
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc();

    // Add application services.
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddTransient<ISmsSender, AuthMessageSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.Configure<MvcOptions>(options =>
    {
        options.Filters.Add(new RequireHttpsAttribute());
    });
}

이메일 공급자 구성하기

본문에서는 Options 패턴을 사용해서 사용자 계정 및 키 설정에 접근합니다. 자세한 정보는 ASP.NET Core 구성하기 문서를 참고하시기 바랍니다.

  • 보안 이메일 키를 가져오기 위한 클래스를 생성합니다. 이번 예제에서는 Services/AuthMessageSenderOptions.cs 라는 파일로 AuthMessageSenderOptions라는 클래스를 생성하겠습니다.
public class AuthMessageSenderOptions
{
    public string SendGridUser { get; set; }
    public string SendGridKey { get; set; }
}

그리고 다음과 같은 명령을 이용해서 Secret-Manager 도구를 사용해서 SendGridUser 정보와 SendGridKey 정보를 설정합니다:

C:\WebApplication3\src\WebApplication3>dotnet user-secrets set SendGridUser RickAndMSFT
info: Successfully saved SendGridUser = RickAndMSFT to the secret store.
노트

원문의 구성에 다소 혼란스러운 면이 존재하는데, 여기에서 얘기하는 SendGridUser 정보와 SendGridKey 정보는 잠시 후에 설명하는 SendGrid 서비스에서 발급받는 정보입니다. 따라서 먼저 SendGrid 서비스부터 등록해야 이 정보를 취득할 수 있습니다. 그리고 2017년 2월 현재, Azure 한국 중부, 서울 및 한국 남부 리전에서는 SendGrid 서비스를 지원하지 않습니다.

Windows에서 Secret Manager는 %APPDATA%/Microsoft/UserSecrets/<userSecretsId> 디렉터리에 위치한 secrets.json 파일에 키/값 쌍을 저장합니다. 이 경로에 사용되는 userSecretsId 디렉터리의 이름은 project.json 파일에서 확인할 수 있습니다. 이를테면 project.json 파일의 처음 몇 줄의 내용은 다음과 비슷할 것입니다:

{
  "webroot": "wwwroot",
  "userSecretsId": "aspnet-WebApplication3-f1645c1b-3962-4e7f-99b2-4fb292b6dade",
  "version": "1.0.0-*",

  "dependencies": {

그리고 현재 secrets.json 파일의 내용은 암호화되지 않으므로 참고하시기 바랍니다. 다음은 secrets.json 파일의 내용을 보여줍니다. (일부 민감한 내용들은 제거되어 있습니다.)

{
  "SendGridUser": "RickAndMSFT",
  "SendGridKey": "",
  "Authentication:Facebook:AppId": "",
  "Authentication:Facebook:AppSecret": ""
}

AuthMessageSenderOptions를 사용하도록 Startup 클래스 구성하기

먼저 project.json 파일에 Microsoft.Extensions.Options.ConfigurationExtensions 의존성이 없으면 추가합니다.

그리고 Startup.cs 파일에 정의된 ConfigureServices 메서드의 끝 부분에 다음의 코드를 추가하여 서비스 컨테이너에 AuthMessageSenderOptions를 추가합니다:

// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);

AuthMessageSender 클래스 구성하기

본문에서는 SendGrid를 이용해서 이메일 알림 기능을 추가하는 방법을 보여주지만, SMTP 및 다른 메커니즘을 이용해서 이메일을 보낼 수도 있습니다.

  • SendGrid.NetCore NuGet 패키지를 설치합니다. 패키지 관리자 콘솔(Package Manager Console)에 다음 명령어를 입력합니다:

    Install-Package SendGrid.NetCore -Pre

노트

SendGrid.NetCore 패키지는 시험판 버전이므로 Install-Package 명령에 -Pre 옵션을 지정해야 합니다.

  • Create a SendGrid account 문서의 지시에 따라서 무료 SendGrid 계정을 등록합니다.

  • Services/MessageServices.cs 파일에 다음과 같은 코드를 추가하여 SendGrid를 구성합니다.

    public class AuthMessageSender : IEmailSender, ISmsSender
    {
        public AuthMessageSender(IOptions<AuthMessageSenderOptions> optionsAccessor)
        {
            Options = optionsAccessor.Value;
        }

        public AuthMessageSenderOptions Options { get; } //set only via Secret Manager

        public Task SendEmailAsync(string email, string subject, string message)
        {
            // Plug in your email service here to send an email.
            var myMessage = new SendGrid.SendGridMessage();
            myMessage.AddTo(email);
            myMessage.From = new System.Net.Mail.MailAddress("Joe@contoso.com", "Joe Smith");
            myMessage.Subject = subject;
            myMessage.Text = message;
            myMessage.Html = message;
            var credentials = new System.Net.NetworkCredential(
                Options.SendGridUser,
                Options.SendGridKey);
            // Create a Web transport for sending email.
            var transportWeb = new SendGrid.Web(credentials);
            return transportWeb.DeliverAsync(myMessage);
        }

        public Task SendSmsAsync(string number, string message)
        {
            // Plug in your SMS service here to send a text message.
            return Task.FromResult(0);
        }
    }
}

계정 확인 및 비밀번호 복구 기능 활성화시키기

이미 템플릿에는 계정 확인 및 비밀번호 복구 기능을 지원하기 위한 코드가 포함되어 있습니다. 다음과 같은 과정을 통해서 이 기능들을 활성화시킵니다:

  • AccountController.cs 파일에서 [HttpPost] Register 메서드를 찾습니다.

  • 계정 확인과 관련된 코드의 주석을 해제하여 기능을 활성화시킵니다.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
        var result = await _userManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713
            // Send an email with this link
            var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
            await _emailSender.SendEmailAsync(model.Email, "Confirm your account",
                $"Please confirm your account by clicking this link: <a href='{callbackUrl}'>link</a>");
            //await _signInManager.SignInAsync(user, isPersistent: false);
            _logger.LogInformation(3, "User created a new account with password.");
            return RedirectToLocal(returnUrl);
        }
        AddErrors(result);
    }

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

다음 코드 라인을 주석으로 처리해서 새로 등록된 사용자가 자동으로 로그인되는 것도 막아야 합니다:

//await _signInManager.SignInAsync(user, isPersistent: false);
  • 동일한 Controllers/AccountController.cs 파일에 정의된 ForgotPassword 액션에서 비밀번호 복구와 관련된 코드의 주석을 해제하여 기능을 활성화시킵니다.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = await _userManager.FindByNameAsync(model.Email);
        if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
        {
            // Don't reveal that the user does not exist or is not confirmed
            return View("ForgotPasswordConfirmation");
        }

        // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713
        // Send an email with this link
        var code = await _userManager.GeneratePasswordResetTokenAsync(user);
        var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
        await _emailSender.SendEmailAsync(model.Email, "Reset Password",
           $"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
        return View("ForgotPasswordConfirmation");
    }

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

마지막으로 Views/Account/ForgotPassword.cshtml 뷰 파일에서 다음에 표시된 ForgotPassword 폼의 주석을 해제합니다.

@model ForgotPasswordViewModel
@{
    ViewData["Title"] = "Forgot your password?";
}

<h2>@ViewData["Title"]</h2>
<p>
    For more information on how to enable reset password please see this <a href="http://go.microsoft.com/fwlink/?LinkID=532713">article</a>.
</p>

<form asp-controller="Account" asp-action="ForgotPassword" method="post" class="form-horizontal">
    <h4>Enter your email.</h4>
    <hr />
    <div asp-validation-summary="All" class="text-danger"></div>
    <div class="form-group">
        <label asp-for="Email" class="col-md-2 control-label"></label>
        <div class="col-md-10">
            <input asp-for="Email" class="form-control" />
            <span asp-validation-for="Email" class="text-danger"></span>
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <button type="submit" class="btn btn-default">Submit</button>
        </div>
    </div>
</form>

@section Scripts {
    @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

등록, 이메일 확인 및 비밀번호 재설정

이번 절에서는 웹 응용 프로그램을 실행해서 계정 확인 및 비밀번호 복구 기능의 흐름을 살펴봅니다.

  • 응용 프로그램을 실행하고 새로운 사용자를 등록합니다.

  • 계정 확인 링크가 포함된 이메일이 도착했는지 확인합니다. 만약 이메일 알림을 받지 못했다면:

    • SendGrid 웹 사이트에서 전송한 이메일 메시지를 확인해봅니다.

    • 스팸 메일 폴더를 확인해봅니다.

    • 다른 이메일 공급자(Microsoft, Yahoo, Gmail 등)에서 제공하는 다른 이메일 주소를 사용해봅니다.

    • SQL Server 개체 탐색기(SSOX, SQL Server Object Explorer)에서 dbo.AspNetUsers 테이블을 열고 해당 이메일 항목을 삭제한 다음 다시 시도해봅니다.

  • 링크를 클릭해서 이메일 주소를 확인합니다.

  • 이메일 주소와 비밀번호를 사용해서 로그인합니다.

  • 로그아웃 합니다.

비밀번호 재설정 테스트

  • 로그인 페이지에서 Forgot your password? 링크를 클릭합니다.

  • 계정 등록에 사용한 이메일 주소를 입력합니다.

  • 비밀번호를 재설정 할 수 있는 링크가 포함된 이메일이 발송됩니다. 이메일을 확인하고 링크를 클릭해서 비밀번호를 재설정합니다. 정상적으로 비밀번호를 재설정하고 나면, 이메일 주소와 새로운 비밀번호를 사용해서 로그인 할 수 있습니다.

로그인 전에 이메일 확인을 필수로 설정하기

본문에서 예제를 생성할 때 선택한 템플릿을 사용할 경우, 사용자가 등록 양식을 작성하고 나면 자동으로 로그인됩니다 (인증됩니다). 그러나 일반적으로는 사용자가 로그인하기 전에 먼저 이메일 확인을 수행하는 것이 좋습니다. 이번 절에서는 새로운 사용자가 로그인 하기 전에 반드시 이메일 확인 절차를 수행해야만 하도록 코드를 변경해보겠습니다. 다음에 강조된 코드를 추가하여 AccountController.cs 파일에 정의된 [HttpPost] Login 액션을 변경합니다.

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;
    if (ModelState.IsValid)
    {
        // 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))
            {
                ModelState.AddModelError(string.Empty, "You must have a confirmed email to log in.");
                return View(model);
            }
        }

        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            _logger.LogInformation(1, "User logged in.");
            return RedirectToLocal(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning(2, "User account locked out.");
            return View("Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return View(model);
        }
    }
노트

보안에 가장 충실한 방식은 운영 환경의 보안 정보를 테스트 환경 및 개발 환경에서는 사용하지 않는 것입니다. 만약 응용 프로그램을 Azure에 게시한다면 보안의 일환으로 SendGrid 보안 정보를 Azure 웹 앱 포털에서 응용 프로그램 설정으로 구성할 수 있습니다. 구성 시스템은 환경 변수에서 키를 읽도록 설정됩니다.

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

이번 절을 마치기 위해서는 먼저 외부 인증 공급자를 활성화시켜야 합니다. Facebook, Google 및 기타 외부 공급자를 이용한 인증 활성화시키기 자습서를 참고하시기 바랍니다.

웹 응용 프로그램 우측 상단의 이메일 링크를 클릭하면 로컬 및 소셜 계정을 결합할 수 있습니다. 다음 일련의 과정은 먼저 로컬 로그인으로 "rickandmsft@gmail.com"이 생성된 것을 가정하고 있지만, 우선 소셜 로그인으로 계정을 생성한 다음 로컬 로그인을 추가할 수도 있습니다.

Manage 링크를 클릭합니다. 현재 이 계정에는 0 개의 외부 계정(소셜 로그인)이 연결되어 있다는 점에 주목하시기 바랍니다.

다른 로그인 서비스에 대한 버튼을 클릭하고 응용 프로그램의 요청을 수락합니다. 다음 그림은 외부 인증 공급자로 Facebook을 사용하고 있는 모습을 보여줍니다:

그러면 두 계정이 결합됩니다. 이제 두 계정 중 한 가지 계정을 이용해서 로그인이 가능합니다. 사용자의 소셜 로그인 인증 서비스가 다운되거나 사용자가 소셜 계정에 대한 접근 권한을 잃는 등의 상황에 대비하여, 이처럼 사용자가 로컬 계정을 추가할 수 있는 기능을 제공해주는 것이 좋습니다.