34. ASP.NET Core 인증과 권한

  • 78 minutes to read

이번 섹션에서는 ASP.NET Core에서 회원 관리를 위한 인증 방법을 살펴봅니다. 여러 가지 인증 방법이 있지만, 쿠키 인증은 배우기 쉽고 사용하기 편리하기 때문에 먼저 공부해보겠습니다. 강의에서는 ASP.NET은 세션 인증을, ASP.NET Core는 쿠키 인증을 기준으로 사용합니다. 이후 ASP.NET Core Identity, IdentityServer, 그리고 Azure AD가 개발되어 SSO(Single Sign On)를 위한 통합된 인증 방법으로 자리매김했습니다.

34.1. ASP.NET Core Identity

ASP.NET Core Identity는 멤버 자격 시스템을 의미합니다. 간단히 말하면 회원 관리 기능을 지칭합니다.

  • ASP.NET Core Identity
    • 멤버 자격 시스템
    • 회원 관리
      • 회원가입, 로그인, 로그아웃, 회원 정보 관리
  • 인증 방식 종류
    • 쿠키 인증
      • 세션 인증: 사용을 권장하지 않음
    • ASP.NET Core Identity
      • JWT(JSON Web Token)
    • IdentityServer
      • OAuth2

34.2. 회원 관리의 주요 범위

회원 관리는 주로 다음과 같은 기능으로 구현됩니다.

  • 회원 가입(Register)
  • 로그인(Login)
    • 로그인 시도 횟수 제한(Lockout)
  • 로그아웃(Logout)
  • 회원 정보 보기/수정(Manage)
    • 프로필 변경
    • 암호 변경
  • 회원 탈퇴

34.2.1. 회원 관리 관련 주요 기능 이름

회원 관리와 관련된 주요 기능 이름은 다음과 같습니다.

  • Login
  • Profile
  • Register
  • Logout

34.3. 인증(Authentication)과 권한(Authorization)

인증(Authentication)은 사용자의 아이디와 암호를 통해 신원을 확인하는 과정으로, 로그인이 성공적으로 이루어졌음을 의미합니다. 권한(Authorization) 또는 허가는 인증된 사용자가 특정 자원에 접근할 수 있는 권리를 부여하는 것으로, 응용 프로그램 내에서 특정 권한에 대한 승인을 의미합니다. 혼동을 방지하기 위해, 책/강의에서는 권한과 허가를 동일한 단어로 사용하겠습니다. 업무적 관점에서 인증과 권한은 다음과 같이 표현될 수 있습니다.

  • 계정 관리(Authentication)
    • 회원 관리
  • 접근 통제(Authorization)
    • 역할 관리
  • 게시판 접근 허가(Permission)
    • 인증된 사용자에게 특정 게시판(페이지)에 대한 사용 허가 부여
  • Accounts
    • 은행에서는 계좌 간의 금액 전송

34.3.1. 인증과 권한 요약

34.3.1.1. 인증: Authentication

  • 사용자의 신원을 확인하는 과정
  • 애플리케이션에서 아이디와 암호가 필요(증거되는 자료 필요)
  • 보호 기능 및 리소스에 대한 접근 제한

참고: Authentication 영어로: Validating who a user claims to be

34.3.1.2. 권한(허가): Authorization

  • 인증된 사용자의 특정 권한 결정(제한된 접근)
  • 권한은 역할(Role)과 같은 다양한 요소를 기반으로 처리
  • 최소 권한 원칙 적용

참고: Authorization 영어로: Giving someone permission to do or have something

사용자 그룹 vs 역할

사용자 그룹과 역할은 모두 권한 관리에 사용되지만, 사용자 그룹은 유사한 권한을 가진 사용자들의 집합을 의미하고, 역할은 특정 권한의 집합을 나타냅니다.

권한(Permission)

  • 역할 기반 권한(Role-based Permission): 사용자 역할에 따른 권한 부여
  • 정책 기반 권한(Policy-based Permission): 규정된 정책에 따른 권한 부여

Single Sign-On(SSO)

  • Trust: 신뢰할 수 있는 여러 시스템 간 인증 정보 공유

프로토콜

  • SAML(Security Assertion Markup Language): XML 기반 인증 및 권한 정보 교환 프로토콜
  • OAuth 2.0: 애플리케이션 간 인증 및 권한 부여 프로토콜, JSON 사용
  • OIDC(OpenID Connect): OAuth 2.0 기반, 추가적인 사용자 정보 제공

SAML

  • XML 기반: 인증 및 권한 정보 교환에 사용되는 언어

OAuth 2.0

  • JSON 기반: 토큰 형식으로 인증 및 권한 정보 교환

OIDC

  • 추가적인 사용자 정보 제공: 기본 OAuth 2.0 프로토콜에 추가 정보를 제공하는 확장

Multi-Factor Authentication(MFA)

  • 두 가지 이상의 인증 방법 사용: 예를 들어 이메일과 문자 메시지를 통한 인증 코드 전송

해싱 알고리즘

해싱 알고리즘은 데이터의 무결성을 검증하고 안전한 암호 저장을 위해 사용됩니다.

  • 이전 방법(사용 금지)
    • MD5: 빠르지만 취약한 해시 함수, 충돌 발생 가능성 있음
    • SHA-1: 보안상 취약점이 발견되어 사용하지 않음
  • 추천 방법
    • SHA-2(SHA-256, SHA-512): 더 강력한 보안을 제공하는 해시 함수
    • BCrypt(Blowfish): 솔트(salt)와 워크 팩터(work factor)를 사용하여 높은 보안 수준 제공

브루트 포스 공격(Brute Force Attack)

브루트 포스 공격은 가능한 모든 암호 조합을 시도하여 인증 시스템을 해킹하는 공격입니다. 이를 방지하기 위해 다음과 같은 방법을 사용할 수 있습니다.

  • 계정 잠금 정책: 연속된 로그인 실패 시 계정을 잠그는 기능
  • CAPTCHA 사용: 자동화된 공격을 차단하기 위해 사용자가 사람임을 증명하도록 요구
  • MFA(Multi-Factor Authentication): 다중 인증 요소를 사용하여 보안 강화

34.4 ASP.NET Core 인증

ASP.NET Core에서 회원 관리를 위한 인증 처리 방식을 구현하는 절차를 살펴봅니다. 이전에 ASP.NET 웹 폼에서 인증(Authentication)과 권한(Authorization)을 다뤘지만, 이번에는 ASP.NET Core에서 인증 방법을 다룹니다. 인증 방식을 간략하게 정리한 후, 실습 예제를 통해 자세한 구현 방법을 살펴봅니다.

  • 인증(Authentication): 사용자의 신원 확인
  • 권한(Authorization): 사용자에게 허용된 리소스 접근 및 작업

인증과 관련된 다른 표현 방법으로는 인증, 승인, 청구, 보안 주체의 4개 단어가 있습니다.

  • Authentication(인증)
    • 사용자가 로그인했는지 여부 확인
  • Authorization(승인, 허가, 권한)
    • 사용자가 로그인한 후, 게시판의 읽기, 쓰기 등 권한이 있는지 확인
  • Claim(청구, 자격)
    • 사용자가 로그인한 후, 어떤 자격들이 있는지 확인
      • 예: 이름, 이메일, 역할(관리자, 일반 사용자, 손님 등)
    • 하나의 인증에 여러 자격을 가질 수 있음
  • ClaimsPrincipal(자격을 지닌 [보안 주체])
    • 인증된 사용자에 대한 정보를 나타냄

34.5 ASP.NET Core의 인증 옵션

34.5.1 템플릿 제공

ASP.NET Core 웹 프로젝트 생성을 위한 기본 템플릿 구성 시 인증 관련 템플릿은 네 가지 항목을 제공합니다. 이번 강의에서는 "인증 안 함" 옵션을 선택하고, 사용자 정의 쿠키 인증을 사용합니다. "개별 사용자 계정"으로 선택하고 인증 부분을 쿠키 인증으로 변경하여 사용할 수도 있습니다.

  • 인증 안 함(No Authentication)
    • 사용자 정의로 직접 인증 처리를 구현합니다. 쿠키 기반 인증을 사용합니다.
  • 개별 사용자 계정(Individual User Accounts)
    • 기본적으로 로컬 DB와 Entity Framework Core 모델을 사용하여 사용자를 정의합니다.
    • 이 옵션에 대한 내용은 ASP.NET Core Identity를 참고합니다.
  • 직장 및 학교 계정(Work and School Accounts)
    • Azure Active Directory와 OpenID Connect 기반 인증입니다.
  • Windows 인증
    • IIS 웹 서버가 필요하고 로컬 로그인한 사용자 기반 인증입니다.

34.5.2 소셜 인증

ASP.NET Core에서는 여러 소셜 사이트에 대한 인증 관련 패키지를 기본으로 제공합니다. 다음 리스트 외에 커뮤니티 기반 인증 방법을 오픈 소스로 제공하고 있습니다.

  • GitHub
  • Facebook
  • Twitter
  • Google
  • Microsoft Account
  • OAuth

34.5.3 ASP.NET Core Identity

ASP.NET Core Identity는 내장된 인증 시스템으로, 회원 가입, 로그인, 로그아웃과 같은 일반적인 회원 관리 기능에 대한 기본 API 및 UI를 제공합니다. ASP.NET Core Identity에 대한 자세한 내용은 다른 강의를 통해 학습합니다.

// 인증 관련 Startup.cs 파일의 참고용 코드
services.AddIdentity<IdentityUser, IdentityRole>();

Identity 뜻

  • User와 같은 의미
    • Person
    • Device
  • Entity와 같은 의미

34.5.4. IdentityServer

.NET Foundation에서 관리하는 IdentityServer는 OAuth 및 OIDC를 위한 인증 기능을 제공합니다. IdentityServer에 대한 자세한 내용은 다른 강의를 통해 학습합니다.

ASP.NET Core에서 사용할 수 있는 인증 방법은 다양하지만 가장 사용하기 쉽고 편리한 방법은 쿠키 인증(Cookie Authentication)입니다. 웹 프로젝트에 쿠키 인증을 사용하려면 NuGet 패키지 관리자를 통해 다음 패키지를 참조로 추가해야 합니다. ASP.NET Core 5.0 버전부터는 이미 포함되어 있습니다.

  • Microsoft.AspNetCore.Authentication.Cookies.dll

34.7 쿠키 인증을 위한 Program.cs(Startup.cs)에 미들웨어 구성 코드 추가

쿠키 인증을 사용하려면 Program.cs(Startup.cs) 파일의 ConfigureServices() 및 Configure() 메서드에 쿠키 인증 관련 미들웨어를 추가해야 합니다. 설정 방법은 버전에 따라 다르며, ASP.NET Core 2.0부터는 다음과 같이 설정할 수 있습니다. ASP.NET Core 6.0 버전 이후로는 builder.Services를 사용하면 됩니다.

// 서비스 등록 영역: builder.Services.___
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();

// 미들웨어 등록 영역: app.___
app.UseAuthentication();

인증이 필요한 컨트롤러 또는 Razor Pages 등에는 [Authorize] 특성을 적용해야 합니다. 인증되지 않은 사용자도 접근이 가능해야 하는 경우에는 [AllowAnonymous] 특성을 사용하면 됩니다.

[Authorize]

쿠키 인증에 대한 기본 설정을 변경하려면, AddCookie() 메서드에 옵션을 전달할 수 있습니다. 예를 들어, 로그인 경로를 변경하거나 쿠키 만료 시간을 설정할 수 있습니다.

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Login";
        options.ExpireTimeSpan = TimeSpan.FromDays(14);
    });

이렇게 설정하면 ASP.NET Core 애플리케이션에서 쿠키 인증을 사용하여 인증 및 권한 관리를 수행할 수 있습니다.

ASP.NET Core 6.0 쿠키 인증 설정

ASP.NET Core 6.0에서의 쿠키 인증 설정은 Program.cs 파일에서 WebApplication.CreateBuilder()를 사용하여 설정하는 것으로 변경되었습니다.

코드: Program.cs 파일의 ConfigureServices() 메서드

var builder = WebApplication.CreateBuilder(args);

// [!] ASP.NET Core 6.0 쿠키 인증: 기본형
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = 
        CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.LoginPath = new PathString("/User/Login");
    options.AccessDeniedPath = new PathString("/User/Forbidden");
});

var app = builder.Build();

코드: Program.cs 파일의 Configure() 메서드

// ASP.NET Core 6.0에서 쿠키 인증
app.UseAuthentication();

app.Run();

ASP.NET Core 6.0 인증 관련 참고 자료

다음 경로를 참고하여 ASP.NET Core 인증 관련 워크샵 자료를 확인할 수 있습니다. 지금은 구 버전이기에 참고용으로만 살펴보세요.

ASP.NET Core 6.0의 쿠키 인증 코드 조각 예제

다음 샘플 코드는 ASP.NET Core에서 쿠키 인증을 적용한 샘플 코드입니다.

코드: D:\SingleSignOn\SingleSignOn.Blazor\Program.cs

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) // "Cookies"
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => // "oidc"
{
    //options.Authority = "https://localhost:44328/"; // Identity Server URI
    options.Authority = "https://localhost:5001/"; // Identity Server URI
    options.ClientId = "6a297776-c6ae-49c6-8cae-e6ef10a92cf0"; // "BlazorClient"

    options.ResponseType = "code id_token";
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.SaveTokens = true;
    options.ClientSecret = "6a297776-c6ae-49c6-8cae-e6ef10a92cf0"; // "secret"
    options.GetClaimsFromUserInfoEndpoint = true;
});

34.8. 쿠키 인증 관련 주요 명령어 정리

쿠키 인증에서 사용되는 주요 명령어를 미리 정리해보았습니다. 간단히 명령어에 대한 코드 조각을 먼저 살펴본 후 실습에서 전체 코드를 만들어보겠습니다. 샘플 코드에서는 아직 작성하지 않은 UserController.cs 파일의 내용으로 코드를 설명합니다.

34.8.1. ClaimsPrincipal 클래스

ASP.NET Core 쿠키 인증은 ClaimsPrincipal 클래스 기반으로 이루어졌습니다.

public virtual ClaimsPrincipal User { get; }

34.8.1.1. 클레임 기반 인증(Claims-Based Authentication)

클레임(Claim)은 아이덴터티에 대한 이름과 값의 쌍을 갖는 속성을 나타냅니다. 아이덴터티는 여러 개의 클레임을 가질 수 있습니다.

  • ClaimsIdentity 클래스는 아이덴터티와 해당 아이덴터티의 클레임을 표현합니다.
  • ClaimsPrincipal 클래스는 하나 이상의 아이덴터티를 가지는 사용자(User)를 표현합니다.

34.8.1.2. Program.cs - ConfigureServices() 메서드

builder.Services.AddAuthentication("Cookies").AddCookie();

- 또는 -

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();

34.8.1.3. Program.cs - Configure() 메서드

app.UseAuthentication();
app.UseAuthorization();

34.8.2. 로그인

로그인 관련 처리는 컨트롤러 클래스의 Login() 같은 액션 메서드에서 다음과 같은 코드 스타일로 처리합니다. Claim 개체의 컬렉션의 항목은 필요한 만큼 추가해서 기록할 수 있습니다.

코드: Login() 액션 메서드의 코드 일부

var claims = new List<Claim>
{
    new Claim("Sub", "1234"),
    new Claim("Name", "박용준"),
    new Claim("Email", "foo@bar.com"),
    new Claim("Role", "Admin"),
    new Claim("Role", "Dev")
};

var ci = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

await HttpContext.SignInAsync("Cookies", new ClaimsPrincipal(ci));

이렇게 생성된 쿠키는 크롬 브라우저의 개발자 도구(F12)의 Application 탭의 Storage 섹션의 Cookies 메뉴를 통해서 살펴볼 수 있습니다. 기본 이름은 ".AspNetCore.Cookies" 입니다.

34.8.2.1. 로그인(쿠키 인증)

HttpContext.SignInAsync(ClaimsPrincipal…); 위 명령어를 실행하면 웹 브라우저에 쿠키가 생성됩니다. await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(ci), authenticationProperties);

34.8.2.2. 로그인 코드 조각

다음은 UserController의 로그인 코드 조각 샘플입니다.

코드:

// UserController.cs – Login() 액션 메서드
//[!] 인증 부여: 인증된 사용자의 주요 정보(Name, Role, ...)를 기록
var claims = new List<Claim>
{
    // 로그인 아이디 지정
    new Claim("UserId", model.UserId),
    new Claim(ClaimTypes.NameIdentifier, model.UserId),
    new Claim(ClaimTypes.Name, model.UserId),

    // 기본 역할 지정, "Role" 기능에 "Users" 값 부여
    new Claim(ClaimTypes.Role, "Users") // 추가 정보 기록
};

var ci = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

//[1] 로그인 처리: Authorize 특성 사용해서 로그인 체크 가능
var authenticationProperties = new AuthenticationProperties
{
    ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(60),
    IssuedUtc = DateTimeOffset.UtcNow,
    IsPersistent = true
};
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(ci), authenticationProperties);

Blazor Server에서 쿠키 인증을 사용하는 코드 조각은 다음 링크를 참고하세요.

Blazor Server 쿠키 인증 코드 조각

34.8.3. 로그인 확인 및 로그인 이름 표시(User Profile)

뷰페이지에서 로그인이 되었는지 안 되었는지를 확인하는 명령어는 User.Identity.IsAuthenticated 속성입니다. 이 값이 true이면 이미 로그인 절차를 거친 사용자를 의미하고, false이면 아직 로그인과 관련된 어떤 정보도 가지고 있지 않은 상태입니다. 로그인 후 클레임 개체에 저장된 데이터를 읽어오는 코드는 User.FindFirst("속성이름").Value 형태로 가져와 사용할 수 있습니다. 다음은 로그인된 상태이면 로그인 값 중에서 Name 값을 출력하고, 로그인하지 않은 상태이면 로그인 페이지로 이동하는 링크를 제공하는 코드입니다.

***코드: ~/Views/Shared/_LoginPartial.cshtml 파일의 코드 일부 ***

@if (User.Identity.IsAuthenticated == true)
{
    <span>@User.FindFirst("Name").Value 님, 반갑습니다.</span>
}
else
{
    <a href="/Home/Login">로그인</a>
}

컨트롤러에서는 다음과 같이 사용자 인증을 확인할 수 있습니다.

  • this.User.Identity.IsAuthenticated

컨트롤러 클래스 등의 코드에서도 아래와 같은 코드로 특정 인증 값을 읽어올 수 있습니다. var username = User.FindFirst("UserId").Value; // 사용자 아이디

  • @User.Claims.FirstOrDefault(x => x.Type == "Name").Value
  • @context.User.Claims.FirstOrDefault(c => c.Type == "")?.Value

34.8.3.1 IHttpContextAccessor

쿠키 인증을 사용할 때 코드 레벨에서 Context에 접근하여 로그인 관련 정보를 얻고자할 때 사용됩니다.

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

if (HttpContextAccessor.HttpContext.User.Identity.IsAuthenticated)
{
    var id = HttpContextAccessor.HttpContext.User.Identity as ClaimsIdentity;
    var email = id.FindFirst("email")?.Value;
}

기타 더 많은 정보는 구글에서 Microsoft Learn + ASP.NET Core Cookie 인증으로 검색해보세요.

34.8.4 로그아웃

로그인이 완료된 인증된 사용자의 인증 쿠키를 삭제하는 명령어는 HttpContext.Authentication.SignOutAsync() 명령어가 사용됩니다. 이 API는 Microsoft.AspNetCore.Authentication 네임스페이스에 포함되어 있습니다.

  • Microsoft.AspNetCore.Authentication.Cookies
    • CookieAuthenticationDefaults.AuthenticationScheme

34.8.4.1 쿠키 인증 로그아웃 코드 조각

***코드: UserController.cs - Logout() 액션 메서드의 코드 일부: 아래 코드가 공식 ***

await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return LocalRedirect(Url.Content(“~/”));

34.8.4.2. 쿠키와 토큰

웹 클라이언트(MVC, Blazor, …)는 쿠키를 사용하고 Web API는 토큰을 사용합니다.

34.8.5. [Authorize] 특성과 [AllowAnonymous] 특성

Microsoft.AspNetCore.Authorization 네임스페이스에 있는 Authorize 특성은 인증되지 않은 사용자의 접근을 거부하도록 컨트롤러 클래스 및 액션 메서드에 지정할 수 있습니다.

34.8.5.1. @attribute

  • 코드 비하인드가 없는 .razor 파일에 [Authorize] 특성을 적용하고자 할 때 사용됩니다.
  • 페이지 상단에 입력해두면 인증된 사용자만이 접근 가능한 구조입니다.

34.8.5.2 [AllowAnonymous] 특성

특정 컴포넌트, 컨트롤러 및 액션 메서드에 로그인하지 않고도 누구나 접속할 수 있게 설정하고자 할 때 사용합니다.

34.8.6. 인증 확인

인증되지 않은 사용자를 로그인 페이지로 리디렉션 시키는 코드는 다음과 같습니다.

***코드: ***

/// <summary>
/// 인사말 페이지
/// </summary>
public IActionResult Greetings()
{
    // [Authorize] 특성의 또 다른 표현 방법
    // 인증되지 않은 사용자는
    if (User.Identity.IsAuthenticated == false)
    {
        // 로그인 페이지로 리디렉션
        return new ChallengeResult(); 
    }

    return View(); 
}

34.8.7 참고: 챌린지 코드 조각

Blazor 또는 MVC 페이지에서 로그인되지 않았을 때 자동으로 지정된 로그인 페이지로 이동할 때에는 ChallengeAsync() 메서드를 호출하면 됩니다.

***코드: ***

//  D:\SingleSignOn\SingleSignOn.Blazor\Pages\LoginSso.cshtml.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace SingleSignOn.Blazor
{
    public class LoginSsoModel : PageModel
    {
        public async Task OnGetAsync()
        {
            if (!HttpContext.User.Identity.IsAuthenticated)
            {
                await HttpContext.ChallengeAsync(
                    OpenIdConnectDefaults.AuthenticationScheme);
            }
            else
            {
                Response.Redirect(Url.Content("~/").ToString());
            }
        }
    }
}

34.8.8 역할 기반 인증

특정 그룹(Group) 또는 역할(Role)에 따라서 권한을 주고자 할 때는 정책 기반으로 인증과 권한을 부여할 수 있습니다. 로그인 시 “Role” 값을 “Users”로 부여하면 해당 사용자는 Users 권한(Policy)을 받습니다. 다음 코드 조각에서 Role에 Users 값을 주는 식으로 사용됩니다.

코드: UserController.cs 파일의 Login 액션 메서드의 코드 일부

// [User][6][6] : 로그인 처리
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(
    UserViewModel model, string returnUrl = null)
{
    if (ModelState.IsValid)
    {
        if (_repository.IsCorrectUser(model.UserId, model.Password))
        {
            // [!] 인증 부여
            var claims = new List<Claim>()
            {
                // 로그인 아이디 지정
                new Claim("UserId", model.UserId),

                // 기본 역할 지정, "Role" 기능에 "Users" 값 부여
                new Claim(ClaimTypes.Role, "Users") // 추가 정보 기록
            };

            var ci = new ClaimsIdentity(claims, model.Password);

            await HttpContext.Authentication.SignInAsync(
                "Cookies", new ClaimsPrincipal(ci));

            return LocalRedirect("/User/Index");
        }
    }

    return View(model);
}

또한, 다음 코드 조각으로 로그인한 UserId가 “Admin”인 사용자만 Administrators 권한이 있는 사용자로 설정할 수 있습니다.

코드: Startup.cs의 ConfigureServices 메서드의 코드 일부

// [User][9] Policy 설정
services.AddAuthorization(options =>
{
    // Users Role이 있으면, Users Policy 부여
    options.AddPolicy(
        "Users", policy => policy.RequireRole("Users"));
    // Users Policy가 있고 UserId가 "Admin"이면 "Administrators" 부여
    options.AddPolicy(
        "Administrators", 
            policy => policy
                .RequireRole("Users")
                .RequireClaim("UserId", "Admin"));
});

다음 코드는 “Administrators” 권한(Policy)이 있는 사용자만 Index 액션 메서드에 접근할 수 있음을 의미합니다.

***코드: ***

// 
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class AdminController : Controller
    {
        [Authorize("Administrators")]
        public string Index()
        {
 
            return "Admin 사용자만 볼 수 있음";
        }
    }
}

34.9 [실습] 초간단 회원 로그인 및 로그아웃, 로그인 정보 표시

34.9.1 소개

현재 실습에 대한 소스는 다음 경로에 있습니다. https://github.com/VisualAcademy/AspNetCoreCookieAuthenticationTest

34.9.2 따라하기 1: 프로젝트 생성 및 기본 코드 확인

(1) DotNetNote 이름으로 ASP.NET Core 6.0 빈 프로젝트를 생성합니다.

(2) Startup.cs 파일의 가장 기본적인 코드 모양입니다.

***코드: ***

//  Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; 
using Microsoft.Extensions.DependencyInjection;

namespace DotNetNote
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }
    }
}

34.9.3 따라하기 2: 로그인 및 로그아웃 기능 구현

(1) 다음은 Startup.cs 파일에 추가로 AuthenticationDemoController 클래스를 만들고, 인증 관련 최소 코드로 로그인과 로그아웃을 구현한 코드입니다.

***코드: ***

// 
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace DotNetNote
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddAuthentication("Cookies").AddCookie();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }
    }

    public class AuthenticationDemoController : Controller
    {
        public async Task<IActionResult> Login()
        {
            var claims = new List<Claim>();

            var claimsIdentity = new ClaimsIdentity(claims, "Cookies");

            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

            await HttpContext.SignInAsync(claimsPrincipal);

            return Content("로그인되었습니다.");
        }

        public async Task<IActionResult> Logout()
        {
            await HttpContext.SignOutAsync("Cookies");
            return Content("로그아웃되었습니다.");
        }
    }
}

(2) 로그인 페이지를 요청하면 쿠키가 생성됩니다.

(3) 로그아웃 페이지를 요청하면 생성되었던 쿠키가 제거됩니다.

34.9.4 따라하기 3: 다양한 옵션 설정

(1) CookieAuthenticationDefaults.AuthenticationScheme 상수를 사용하여 “Cookies” 문자열을 대체합니다.

***코드: ***

// 
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace DotNetNote
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services
                .AddAuthentication(
                    CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }
    }

    public class AuthenticationDemoController : Controller
    {
        public async Task<IActionResult> Login()
        {
            var claims = new List<Claim>();

            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);

            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

            await HttpContext.SignInAsync(claimsPrincipal);

            return Content("로그인되었습니다.");
        }

        public async Task<IActionResult> Logout()
        {
            await HttpContext.SignOutAsync(
                CookieAuthenticationDefaults.AuthenticationScheme);
            return Content("로그아웃되었습니다.");
        }
    }
}

(2) 로그인 기능 처리할 때 좀 더 다양한 클레임을 주거나, 추가적인 인증 옵션을 적용할 수 있습니다.

***코드: ***

// 
public async Task<IActionResult> Login()
{
    var claims = new List<Claim>
    { 
        new Claim(ClaimTypes.NameIdentifier, "UserId"),
        new Claim(ClaimTypes.Name, "UserName"),
        new Claim(ClaimTypes.Email, "UserEmail"),
        new Claim(ClaimTypes.Role, "Users"),
        new Claim("원하는 이름", "원하는 값"),
    };

    var claimsIdentity = new ClaimsIdentity(
        claims, CookieAuthenticationDefaults.AuthenticationScheme);

    var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

    await HttpContext.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme,
        claimsPrincipal,                
        new AuthenticationProperties { IsPersistent = false });

    return Content("로그인되었습니다.");
}

34.9.5 따라하기 4: 로그인 확인 및 로그인 정보 표시

(1) 인증된 사용자에 대한 정보를 표시하는 기능을 추가합니다.

***코드: ***

// 
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace DotNetNote
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services
                .AddAuthentication(
                    CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }
    }

    public class AuthenticationDemoController : Controller
    {
        public async Task<IActionResult> Login()
        {
            var claims = new List<Claim>
            { 
                new Claim(ClaimTypes.NameIdentifier, "UserId"),
                new Claim(ClaimTypes.Name, "UserName"),
                new Claim(ClaimTypes.Email, "UserEmail"),
                new Claim(ClaimTypes.Role, "Users"),
                new Claim("원하는 이름", "원하는 값"),
            };

            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);

            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                claimsPrincipal,                
                new AuthenticationProperties { IsPersistent = false });

            return Content("로그인되었습니다.");
        }

        public async Task<IActionResult> Logout()
        {
            await HttpContext.SignOutAsync(
                CookieAuthenticationDefaults.AuthenticationScheme);
            return Content("로그아웃되었습니다.");
        }

        public IActionResult LoginPartial()
        {
            string result = "";
            if (User.Identity.IsAuthenticated)
            {
                result = $"<h3>{User.Identity.Name}</h3>";
                foreach (var claim in User.Claims)
                {
                    result += $"{claim.Type} - {claim.Value}<br />";
                }
            }
            else
            {
                result = "로그인하지 않았습니다.";
            }
            return Content(result, "text/html", Encoding.Default);
        }
    }
}

(2) LoginPartial 액션 페이지를 요청하면 인증된 정보가 표현됩니다.

34.9.6 마무리

이번 실습은 최소한의 코드로 로그인, 로그아웃, 로그인 정보 표시를 진행하였습니다.

34.10. [실습] ASP.NET Core 쿠키 인증으로 회원 관리 기능 구현하기

소개

ASP.NET Core에서 쿠키 인증 방식을 사용하여 회원 가입, 로그인, 로그아웃, 로그인 상태 확인 등의 기능을 구현해보겠습니다. 추가로 암호 변경, 프로필 변경 등의 작업이 필요하겠지만, 이번 실습에서 이 두 가지 기능은 구현하지 않습니다. 실습에서 제작할 회원 인증과 관련된 전체 프로젝트의 완성된 구조는 다음과 같습니다. 그림 34 3 인증 관련 폴더 및 파일 구조

ASP.NET Core 6.0 버전의 쿠키 인증 방식에 대한 데모 소스는 다음 경로에서 다운로드 받을 수 있습니다. 처음 책 집필시에 쿠키 인증으로 관리자 기능 구현하는 부분은 제거 했습니다. 역할 기반 인증은 쿠키 인증 대신에 ASP.NET Core Identity 강의로 그 내용을 위임했습니다.

따라하기 1: SQL Server 데이터베이스 프로젝트 확인 및 참조 추가

(1) ASP.NET Core 회원 관리 기능에서 데이터베이스 구조는 웹 폼(One ASP.NET) 방식으로 구현해 본 DevUser 프로젝트에서의 테이블 구조와 저장 프로시저를 그대로 사용할 것입니다. 이는 데이터베이스 프로젝트인 DotNetNote.Database 프로젝트에 다음과 같이 두 파일로 들어 있습니다.

  • DotNetNote.Database SQL Server 데이터베이스
    • 테이블(Users.sql)
    • 저장 프로시저(Users_Procedures.sql)

(2) DotNetNote.Database 프로젝트의 dbo 폴더의 Tables 폴더에 있는 Users.sql 파일과 UserLogs.sql 파일을 SQL Server에 설치합니다. Users 테이블을 생성하는 구문은 다음과 같습니다. 만약, 아래 sql 파일이 없으면 새롭게 생성합니다.

코드: DotNetNote.Database/dbo/Tables/Users.sql

--[User][0][1] 회원관리를 위한 Users 테이블 생성

--[0] Users 테이블 생성
Create Table dbo.Users
(
    UID Int Identity(1, 1) Primary Key Not Null,
    UserID NVarChar(25) Not Null,
    -- 암호 필드를 20자에서 255자로 변경(암호화때문에)...
    [Password] NVarChar(255) Not Null
    -- 필요한 항목이 있으면, 언제든 추가
	, Username NVarChar(20) Null	-- 사용자 이름
)
Go

----[1] 입력 예시문
--Insert Into Users Values('admin', '1234')
----[2] 출력 예시문
--Select * From Users Order By UID Desc
----[3] 상세 예시문
--Select * From Users Where UID = 1
----[4] 수정 예시문
--Begin Tran
--	Update Users
--	Set	
--		UserID = 'redplus',
--		Password = '1234'
--	Where UID = 1
--Commit Tran
----[5] 삭제 예시문
--Delete Users Where UID = 1
----[6] 검색 예시문
--Select * From Users Where UserID Like '%red%'
--GO

5번 로그인 실패했을 때 10분 정도 접근을 금지할 수 있는 정보를 저장하는 UserLogs.sql 파일은 다음과 같습니다.

***코드: ***

//  UserLogs.sql
-- 로그인 실패 시도 등의 정보 저장
CREATE TABLE [dbo].[UserLogs]
(
	-- 일련번호
	[Id] INT NOT NULL PRIMARY KEY Identity(1, 1),		
	-- 사용자 아아디
	Username NVarChar(50) Not Null,						
	-- 로그인 실패 카운트
	FailedPasswordAttemptCount Int Default(0),			
	-- 로그인 실패 처음 생성일
	FailedPasswordAttemptWindowStart 
		DateTime Default(GetDate())						
)
Go

(3) DotNetNote.Database 프로젝트의 dbo 폴더의 Stored Procedures 폴더에 있는 Users_Procedures.sql 파일을 실행합니다. 회원 관리 관련 저장 프로시저를 생성하는 구문은 다음과 같습니다. 마찬가지로 아래 sql 파일이 없으면 새롭게 생성합니다.

***코드: ***

//  DotNetNote.Database/dbo/Stored Procedures/Users_Procedures.sql
--[User][0][2] Users 관련 저장 프로시저 생성

--[1] 입력 저장 프로시저
Create Proc dbo.WriteUsers
	@UserID NVarChar(25),
	@Password NVarChar(255)
As
	Insert Into Users (UserID, Password) Values(@UserID, @Password)
Go
--WriteUsers 'redplus', '1234'


--[2] 출력 저장 프로시저
--Create Proc dbo.ListUsers
--As
--	Select * From Users Order By UID Desc
--Go
Create Proc dbo.ListUsers
As
	Select [UID], [UserID], [Password] From Users Order By UID Desc
Go
--ListUsers


--[3] 상세 저장 프로시저
Create Proc dbo.ViewUsers
	@UID Int
As
	Select [UID], [UserID], [Password] From Users Where UID = @UID
Go
--ViewUsers 5


--[4] 수정 저장 프로시저
Create Proc dbo.ModifyUsers
	@UserID NVarChar(25),
	@Password NVarChar(255),
	@UID Int
As
	Begin Tran
		Update Users
		Set	
			UserID = @UserID,
			[Password] = @Password
		Where UID = @UID
	Commit Tran
Go
--ModifyUsers 'master', '1234', 2


--[5] 삭제 저장 프로시저
Create Proc dbo.DeleteUsers
	@UID Int
As
	Delete Users Where UID = @UID
Go
--DeleteUsers 2


--[6] 검색 저장 프로시저
Create Proc dbo.SearchUsers
	@SearchField NVarChar(25),
	@SearchQuery NVarChar(25)
As
	Declare @strSql NVarChar(255)
	Set @strSql = '
		Select * From Users 
		Where 
			' + @SearchField + ' Like ''%' + @SearchQuery + '%''
	'
	-- Print @strSql
	Exec(@strSql)
Go
--SearchUsers 'UserID', 'admin'

사용자에 대한 상세정보에 대한 저장 프로시저는 다음과 같습니다.

***코드: ***

//  DotNetNote.Database\Users\Stored Procedures\GetUsers.sql
CREATE PROCEDURE [dbo].[GetUsers]
	@UID int = 0
AS
	Select [UID], [UserID], [Password], [Username] 
	From Users 
	Where UID = @UID
Go

(2) DotNetNote.Database 프로젝트를 SQL Server LocalDB의 DotNetNote 데이터베이스로 게시합니다. 이에 대한 데이터베이스 연결 문자열 정보는 DotNetNote 웹 프로젝트 루트에 있는 appsettings.json 파일에 기록되어 있습니다.

***코드: ***

// 
{
    "ConnectionStrings": {
        "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=DotNetNote;Trusted_Connection=True;"
    },
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information"
        }
    }
}

***코드: ***

//  D:\DotNetNoteCookies\DotNetNote\DotNetNote\appsettings.json
{
  "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=DotNetNoteCookies;Trusted_Connection=True;",
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=DotNetNoteCookies;Trusted_Connection=True;"
  },
  "Data": {
    "DefaultConnection": {
      "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=DotNetNoteCookies;Trusted_Connection=True;"
    }
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

(3) 참조 추가를 위해 DotNetNote 웹 프로젝트를 열고, NuGet 패키지 관리자를 사용하여 쿠키 인증 관련 패키지를 추가합니다. 쿠키 인증을 사용하려면 Cookies 패키지가 미리 추가되어야 합니다. 물론, 3.1 버전에서는 더 이상 패키지를 추가할 필요가 없습니다.

***코드: ***

//  NuGet 패키지 관리자를 사용하여 패키지 추가
"Microsoft.AspNetCore.Authentication.Cookies": "1.1.1",

(4) Startup.cs 파일의 전체 구조입니다. 쿠키 인증 관련된 전체 코드 목록입니다. [5]번 파트는 아직 Repository 클래스를 만들기 전이지만, 기록차원에서 모두 넣어두었습니다.

***코드: ***

//  DotNetNote\DotNetNote\Startup.cs
using DotNetNote.Common;
using DotNetNote.Components;
using DotNetNote.Models;
using DotNetNote.Settings;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DotNetNote
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            //[!] Configuration: JSON 파일의 데이터를 POCO 클래스에 주입
            services.Configure<DotNetNoteSettings>(
                Configuration.GetSection("DotNetNoteSettings"));

            services.AddAuthentication("Cookies")
                .AddCookie(options =>
                {
                    options.LoginPath = "/User/Login/";
                    options.AccessDeniedPath = "/User/Forbidden/";
                });

            //[DI] 의존성 주입(Dependency Injection)
            DependencyInjectionContainer(services);
        }

        /// <summary>
        /// 의존성 주입 관련 코드만 따로 모아서 관리
        /// - 리포지토리 등록
        /// </summary>
        private void DependencyInjectionContainer(IServiceCollection services)
        {
            //[?] ConfigureServices가 호출되기 전에는 DI(종속성 주입)가 설정되지 않습니다.

            //[DNN][!] Configuration 개체 주입: 
            //    IConfiguration 또는 IConfigurationRoot에 Configuration 개체 전달
            //    appsettings.json 파일의 데이터베이스 연결 문자열을 
            //    리포지토리 클래스에서 사용할 수 있도록 설정
            // IConfiguration 주입 -> Configuration의 인스턴스를 실행 
            services.AddSingleton<IConfiguration>(Configuration);

            //[User][5] 회원 관리
            services.AddTransient<IUserRepository, UserRepository>();
            // LoginFailedManager
            services.AddTransient<ILoginFailedRepository, LoginFailedRepository>();
            services.AddTransient<ILoginFailedManager, LoginFailedManager>();
            // 사용자 정보 보기 전용 컴포넌트
            services.AddTransient<IUserModelRepository, UserModelRepository>();

            //[User][9] Policy 설정
            services.AddAuthorization(options =>
            {
                // Users Role이 있으면, Users Policy 부여
                options.AddPolicy("Users", policy => policy.RequireRole("Users"));

                // Users Role이 있고 UserId가 DotNetNoteSettings:SiteAdmin에 
                // 지정된 값(예를 들어 "Admin")이면 "Administrators" 부여
                // "UserId" - 대소문자 구분
                options.AddPolicy("Administrators", policy => 
                    policy.RequireRole("Users").RequireClaim("UserId", 
                        Configuration.GetSection("DotNetNoteSettings")
                            .GetSection("SiteAdmin").Value));
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication(); 
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

(5) Startup.cs 클래스의 ConfigureServices() 메서드에 최고 관리자 아이디에 대한 정보를 Settings 폴더의 DotNetNoteSettings.json 파일에서 읽어와서 Administrators 정책 권한을 설정하는 코드를 추가합니다. 다음 코드는 ConfigureServices() 메서드의 모든 코드를 표시한 것은 아닙니다.

***코드: ***

//  Startup.cs 클래스의 ConfigureServices() 메서드에 코드 추가
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<DotNetNoteSettings>(
        Configuration.GetSection("DotNetNoteSettings"));


    // [User][9] Policy 설정
    services.AddAuthorization(options =>
    {
        // Users Role이 있으면, Users Policy 부여
        options.AddPolicy(
            "Users", policy => policy.RequireRole("Users"));
        // Users Role이 있고 UserId가 "Admin"이면 "Administrators" 부여
        options.AddPolicy(
            "Administrators", 
                policy => policy
                    .RequireRole("Users")
                    .RequireClaim("UserId", // 대소문자 구분
                        Configuration
                            .GetSection("DotNetNoteSettings")
                            .GetSection("SiteAdmin").Value)
                    ); 
    });

    
}

34.10.3 따라하기 2: 모델 클래스와 리포지토리 클래스 구현

(1) 모델 클래스와 리포지토리 클래스를 구현해보도록 합니다.

  • 모델 클래스
    • UserViewModel.cs
  • 리포지토리 클래스
    • IUserRepository.cs
    • UserRepository.cs

처음 집필시 작업은 /Models/ 폴더에서 작업했지만, 배포용 최종 DotNetNote 소스에는 /Models/User/ 폴더에 3개의 cs 파일이 위치합니다. 비밀번호 길이도 처음에는 20자로 제한했지만, 비밀번호에 대한 추가 암호화때문에 최종 255자로 길이를 설정했습니다.

(2) DotNetNote 프로젝트의 Models 폴더에 UserViewModel.cs 파일로 모델 클래스를 생성하고, 다음과 같이 코드를 입력합니다. Display, Required, StringLength 특성을 사용하여 유효성 검사를 진행합니다. UserViewModel 클래스는 SQL Server의 Users 테이블과 일대일 구조입니다. Users 테이블에는 UID로 컬럼명을 지정하고 있지만, C# 클래스에서는 Id로 사용하도록 하겠습니다.

***코드: ***

//  DotNetNote 프로젝트 - /Models/UserViewModel.cs
// [User][2]
using System.ComponentModel.DataAnnotations;

namespace DotNetNote.Models
{
    public class UserViewModel
    {
        public int Id { get; set; }

        [Display(Name = "아이디")]
        [Required(ErrorMessage = "아이디를 입력하시오.")]
        [StringLength(25, MinimumLength = 3, 
            ErrorMessage = "아이디는 3자 이상 25자 이하로 입력하시오.")]
        public string UserId { get; set; }

        [Display(Name = "비밀번호")]
        [Required(ErrorMessage = "비밀번호를 입력하시오.")]
        [StringLength(255, MinimumLength = 6, 
            ErrorMessage = "비밀번호는 6자 이상 255자 이하로 입력하시오.")]
        public string Password { get; set; }
    }
}

(3) Models 폴더에 IUserRepository.cs 파일로 인터페이스를 생성하고 다음 코드와 같이 메서드 시그니처를 작성하여 리포지토리 클래스에서 상속받아 사용하도록 합니다.

***코드: ***

//  DotNetNote 프로젝트 - /Models/IUserRepository.cs
//[User][3]
namespace DotNetNote.Models
{
    public interface IUserRepository
    {
        void AddUser(string userId, string password);
        UserViewModel GetUserByUserId(string userId);
        bool IsCorrectUser(string userId, string password);
        void ModifyUser(int uid, string userId, string password);
    }
}

(4) Models 폴더에 회원 테이블에 데이터를 입출력할 수 있는 데이터베이스 담당 클래스인 리포지토리 클래스를 UserRepository.cs 파일명으로 작성하여 다음과 같이 구현합니다. 참고로 데이터베이스 연결 문자열은 appsettins.json 파일에 설정되어 있습니다. 현재 리포지토리 클래스를 구현하기 위해서 NuGet 갤러리에서 다음 패키지를 설치합니다.

  • PM> Install-Pakcage Microsoft.Data.SqlClient

***코드: ***

//  DotNetNote 프로젝트 - /Models/UserRepository.cs
//[User][4]
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using System.Data;

namespace DotNetNote.Models
{
    public class UserRepository : IUserRepository
    {
        private IConfiguration _config;
        private SqlConnection con;

        public UserRepository(IConfiguration config)
        {
            _config = config;
            // appsettings.json 파일에 설정된 데이터베이스 연결 문자열 읽어오기
            con = new SqlConnection(
                _config.GetSection("ConnectionStrings")
                    .GetSection("DefaultConnection").Value);
        }

        /// <summary>
        /// 회원 가입
        /// </summary>
        public void AddUser(string userId, string password)
        {
            SqlCommand cmd = new SqlCommand();
            cmd.Connection = con;
            cmd.CommandText = "WriteUsers";
            cmd.CommandType = CommandType.StoredProcedure;

            cmd.Parameters.AddWithValue("@UserID", userId);
            cmd.Parameters.AddWithValue("@Password", password);

            con.Open();
            cmd.ExecuteNonQuery();
            con.Close();
        }

        /// <summary>
        /// 특정 회원 정보
        /// </summary>
        public UserViewModel GetUserByUserId(string userId)
        {
            UserViewModel r = new UserViewModel();

            SqlCommand cmd = new SqlCommand();
            cmd.Connection = con;
            cmd.CommandText = "Select * From Users Where UserID = @UserID";
            cmd.CommandType = CommandType.Text;

            cmd.Parameters.AddWithValue("@UserID", userId);

            con.Open();
            IDataReader dr = cmd.ExecuteReader();
            if (dr.Read())
            {
                r.Id = dr.GetInt32(0);
                r.UserId = dr.GetString(1);
                r.Password = dr.GetString(2);
            }
            con.Close();

            return r;
        }


        /// <summary>
        /// 회원 정보 수정
        /// </summary>
        public void ModifyUser(int uid, string userId, string password)
        {
            SqlCommand cmd = new SqlCommand();
            cmd.Connection = con;
            cmd.CommandText = "ModifyUsers";
            cmd.CommandType = CommandType.StoredProcedure;

            cmd.Parameters.AddWithValue("@UserID", userId);
            cmd.Parameters.AddWithValue("@Password", password);
            cmd.Parameters.AddWithValue("@UID", uid);

            con.Open();
            cmd.ExecuteNonQuery();
            con.Close();
        }

        /// <summary>
        /// 아이디와 암호가 동일한 사용자면 참(true) 그렇지 않으면 거짓
        /// </summary>
        public bool IsCorrectUser(string userId, string password)
        {
            bool result = false;

            con.Open();
            SqlCommand cmd = new SqlCommand();
            cmd.Connection = con;
            cmd.CommandText = "Select * From Users "
                + " Where UserID = @UserID And Password = @Password";
            cmd.CommandType = CommandType.Text;
            cmd.Parameters.AddWithValue("@UserID", userId);
            cmd.Parameters.AddWithValue("@Password", password);
            SqlDataReader dr = cmd.ExecuteReader();
            if (dr.Read())
            {
                result = true; // 아이디와 암호가 맞는 데이터가 있구나...
            }
            con.Close();
            return result;
        }
    }
}

더 많은 메서드가 구현되어야 하지만, 회원 가입 및 로그인에 필요한 정보만을 API로 구현하여 최소한의 회원 인증을 보여주었습니다.

(5) Startup.cs 파일의 ConfigureServices() 메서드에 다음 코드가 등록되었는지 확인합니다. 만약 새롭게 프로젝트를 만들어 연습해서 코드가 없다면 작성하도록 합니다. IConfiguration 인터페이스는 UserRepository.cs 파일의 생성자에서 사용됩니다. IConfiguration 매개변수를 사용하여 프로젝트 루트에 있는 appsettings.json 파일에 설정된 데이터베이스 연결 문자열을 얻을 수 있습니다. 이때 IConfiguration 개체의 인스턴스 생성 시 Configuration 개체를 전달하는 코드를 Startup.cs 파일의 ConfigureServcies() 메서드에 다음과 같이 반드시 등록해 주어야 합니다.

***코드: ***

//  Startup.cs 파일의 ConfigureServices() 메서드의 코드 확인
services.AddSingleton<IConfiguration>(Configuration);

(6) Startup.cs 파일의 ConfigureServices() 메서드 제일 하단에 다음 코드 한 줄을 추가로 작성합니다. 회원 관리 관련 컨트롤러의 생성자에서 IUserRepository 인터페이스 매개변수에 대한 인스턴스 생성은 UserRepository를 설정하기 위한 코드를 다음 코드처럼 services.AddTransient<인터페이스, 클래스>() 형태로 입력합니다.

***코드: ***

//  Startup.cs – ConfigureServices()
//[User][5] 회원 관리
services.AddTransient<IUserRepository, UserRepository>();

34.10.4 따라하기 3: 각각의 컨트롤러 및 페이지 구성

(1) 회원 가입 처리를 위한 컨트롤러와 각각의 뷰 페이지를 구성합니다.

  • 회원 가입: /User/Register
  • 로그인: /User/Login
  • 메인 페이지: /User/Index
  • 로그아웃: /User/Logout

(2) 회원 인증 처리를 위한 컨트롤러 클래스인 UserController.cs 파일을 생성하고 다음과 같이 코드를 작성합니다.

  • UserController 생성자: 리포지토리 클래스를 생성자 주입시킵니다.
  • Index 액션 메서드: [Authorize] 특성을 적용하여 인증되지 않은 사용자는 Startup.cs 파일에서 인증 설정 시 준 옵션에 의해서 Login 페이지로 자동 이동시킵니다.
  • Register 액션 메서드: 회원 가입 관련된 코드를 작성합니다.
  • Login 액션 메서드: 쿠키 인증을 사용하여 아이디와 암호가 맞으면 인증을 줍니다.
  • Logout 액션 메서드: 쿠키 인증을 제거하여 로그아웃 기능을 구현합니다.

최근 DotNetNote 소스에는 DotNetNote.Common 폴더에 암호화를 위한 CryptoEngine.cs와 로그인 실패 처리 기능을 위한 LoginFailedManager.cs가 추가되었습니다. 소스로는 /Common/ 폴더가 추가되었습니다.

***코드: ***

//  Controllers/UserController.cs
// [User][6]
using DotNetNote.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
 
namespace DotNetNote.Controllers
{
    public class UserController : Controller
    {
        // [User][6][1]
        private IUserRepository _repository;
 
        public UserController(IUserRepository repository)
        {
            _repository = repository;
        }
        
 
        // [User][6][2]
        [Authorize]
        public IActionResult Index()
        {
            return View();
        }
 
 
        // [User][6][3] : 회원 가입 폼
        [HttpGet]
        public IActionResult Register()
        {
            return View();
        }
 
        // [User][6][4] : 회원 가입 처리
        [HttpPost]
        public IActionResult Register(UserViewModel model)
        {
            if (ModelState.IsValid)
            {
                if (_repository.GetUserByUserId(model.UserId).UserId != null)
                {
                    ModelState.AddModelError("", "이미 가입된 사용자입니다.");
                    return View(model);
                }
            }
 
            if (!ModelState.IsValid)
            {
                ModelState.AddModelError("", "잘못된 가입 시도!!!");
                return View(model);
            }
            else
            {
                _repository.AddUser(model.UserId, model.Password);
                return RedirectToAction("Index");
            }
        }
 
 
        // [User][6][5] : 로그인 폼
        [HttpGet]
        [AllowAnonymous] // 인증되지 않은 사용자도 접근 가능
        public IActionResult Login(string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            return View();
        }
 
        // [User][6][6] : 로그인 처리
        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> Login(
            UserViewModel model, string returnUrl = null)
        {
            if (ModelState.IsValid)
            {
                if (_repository.IsCorrectUser(model.UserId, model.Password))
                {
                    // [!] 인증 부여
                    var claims = new List<Claim>()
                    {
                        // 로그인 아이디 지정
                        new Claim("UserId", model.UserId),
 
                        // 기본 역할 지정, "Role" 기능에 "Users" 값 부여
                        new Claim(ClaimTypes.Role, "Users") // 추가 정보 기록
                    };
 
                    var ci = new ClaimsIdentity(claims, model.Password);
 
                    await HttpContext.Authentication.SignInAsync(
                        "Cookies", new ClaimsPrincipal(ci));
 
                    return LocalRedirect("/User/Index");
                }
            }
 
            return View(model);
        }
 
 
        // [User][6][7] : 로그아웃 처리
        public async Task<IActionResult> Logout()
        {
            // Startup.cs의 미들웨어에서 지정한 "Cookies" 이름 사용
            await HttpContext.Authentication.SignOutAsync("Cookies");
 
            return Redirect("/User/Index");
        }
 
 
        // [User][6][8] : 회원 정보 보기 및 수정
        [Authorize]
        public IActionResult UserInfor()
        {
            return View();
        }
 
        // [User][6][9] : 인사말 페이지
        public IActionResult Greetings()
        {
            // [Authorize] 특성의 또 다른 표현 방법
            // 인증되지 않은 사용자는
            if (User.Identity.IsAuthenticated == false)
            {
                // 로그인 페이지로 리디렉션
                return new ChallengeResult();
            }
 
            return View();
        }
 
        // [User][6][10] : 접근 거부 페이지
        public IActionResult Forbidden()
        {
            return View();
        }
    }
}

(3) 컨트롤러 클래스가 완성되었으면 컨트롤러 클래스의 각각의 액션 메서드에 해당하는 뷰 페이지를 작성합니다. 먼저 Views 폴더에 User 폴더를 생성하고, Index.cshtml 이름으로 MVC 뷰 페이지를 생성한 후 다음과 같이 코드를 작성합니다. Index 페이지는 인증되지 않은 사용자는 접근이 불가능한 페이지입니다. 인증되고 나면 앞서 코드 조각으로 살펴본 인증된 사용자에 대한 인증 이름이 출력되는 코드를 구현하였습니다. 다음은 테스트를 위한 코드이고, 실제 사용을 위한 코드는 뒤에서 구현할 _LoginPartial.cshtml 코드입니다.

***코드: ***

//  /Views/User/Index.cshtml
@*// [User][7] 회원 정보 표시*@

<h1>메인 페이지</h1>

@if (User.Identity.IsAuthenticated)
{
    <span>@User.FindFirst("UserId").Value 님, 반갑습니다.</span>

    <a href="/User/Logout">로그아웃</a>
}
else
{
    <a href="/User/Login">로그인</a>

    <a href="/User/Register">회원가입</a>
}
@User.Claims.FirstOrDefault(x => x.Type == “Name”).Value 

(4) 회원 가입 및 로그인 뷰 페이지에서 사용할 유효성 검사 관련 jQuery 플러그인에 대한 부분 페이지를 생성합니다. DotNetNote 프로젝트의 루트에 있는 Views 폴더의 Shared 폴더에 _ValidationScriptsPartial.cshtml 파일명으로 뷰 페이지를 생성하고 다음과 같이 코드를 작성합니다. 참고로 이 파일은 ASP.NET Core 프로젝트 생성시 웹 응용 프로그램 템플릿을 사용하고 인증을 개별 사용자 인증으로 선택하면 기본으로 생성되는 파일입니다. 우리는 DotNetNote 웹 프로젝트를 인증 안 함 옵션으로 생성하였기에 기본적으로 이 파일은 생성되지 않습니다.

***코드: ***

//  /Views/Shared/_ValidationScriptsPartial.cshtml
<environment names="Development">
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src=
        "~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js">
    </script>
</environment>
<environment names="Staging,Production">
    <script src=
            "https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js"
        asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
        asp-fallback-test="window.jQuery && window.jQuery.validator">
    </script>
    <script src=
            "https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js"
        asp-fallback-src=
            "~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
        asp-fallback-test=
            "window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive">
    </script>
</environment>

(5) Views 폴더의 User 폴더에 회원 가입을 처리하는 뷰 페이지 Register.cshtml 페이지를 만들고 다음과 같이 작성합니다.

***코드: ***

//  /Views/User/Register.cshtml
@model UserViewModel
 
@{
    ViewData["Title"] = "회원가입";
}
 
<h2>@ViewData["Title"].</h2>
 
<div class="row">
<div class="col-md-8">
<section>
<form asp-controller="User" asp-action="Register" method="post" 
      class="form-horizontal">
<h4>아래 항목을 입력하시오.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
    <label class="col-sm-2 control-label" asp-for="UserId"></label>
    <div class="col-sm-6">
        <input type="text" class="form-control" asp-for="UserId" 
               placeholder="아이디">
        <span asp-validation-for="UserId" class="text-danger"></span>
    </div>
</div>
<div class="form-group">
    <label class="col-sm-2 control-label" asp-for="Password"></label>
    <div class="col-sm-6">
        <input type="password" class="form-control" asp-for="Password" 
               placeholder="암호">
        <span asp-validation-for="Password" class="text-danger"></span>
    </div>
</div>
<div class="form-group">
    <div class="col-sm-offset-2 col-sm-6">
        <input type="submit" value="가입" class="btn btn-primary btn-lg" />
        <a asp-controller="User" asp-action="Index" 
           class="btn btn-default btn-sm">취소</a>
    </div>
</div>
</form>
</section>
</div>
<div class="col-md-4">
</div>
</div>
 
@section Scripts {
    @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}
@section Scripts {
    @*// _Layout.cshtml 페이지의 Scripts 이름의 섹션에 들어갈 영역*@
    <script>
        $(function() {
            
        });
    </script>
}

(6) 같은 경로에 로그인을 담당하는 뷰 페이지인 Login.cshtml을 생성하고 다음과 같이 작성합니다.

***코드: ***

//  /Views/User/Login.cshtml
@model UserViewModel
 
@{
    ViewData["Title"] = "로그인";
}
 
<h2>@ViewData["Title"].</h2>
<div class="row">
<div class="col-md-8">
<section>
<form asp-controller="User" asp-action="Login" class="form-horizontal"
      asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">
<h4>아이디와 암호를 입력하시오.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
    <label asp-for="UserId" class="col-md-2 control-label"></label>
    <div class="col-md-6">
        <input asp-for="UserId" class="form-control" placeholder="아이디" />
        <span asp-validation-for="UserId" class="text-danger"></span>
    </div>
</div>
<div class="form-group">
    <label asp-for="Password" class="col-md-2 control-label"></label>
    <div class="col-md-6">
        <input asp-for="Password" class="form-control" 
               type="password" placeholder="암호" />
        <span asp-validation-for="Password" class="text-danger"></span>
    </div>
</div>
<div class="form-group">
    <div class="col-md-offset-2 col-md-6">
        <button type="submit" class="btn btn-primary btn-lg">로그인</button>
 
        <a asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" 
           class="btn btn-default btn-sm">회원가입</a>
    </div>
</div>
</form>
</section>
</div>
<div class="col-md-4">
</div>
</div>
 
@section Scripts {
    @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

(7) 같은 경로에 로그아웃을 담당하는 뷰 페이지를 Logout.cshtml 파일명으로 다음과 같이 간단히 작성합니다.

***코드: ***

//  /Views/User/Logout.cshtml
<h2>로그아웃</h2>
await HttpContext.Authentication.SignOutAsync(“Cookies”);
await HttpContext.Authentication.SignOutAsync(“Oidc”);

(8) 메인 레이아웃 페이지의 오른쪽 상단에 포함되어 사용될 공통 페이지인 _LoginPartial.cshtml 파일을 Views/Shared 폴더에 생성하고 다음과 같이 코드를 작성합니다.

  • User.Identity.IsAuthenticate

코드: /Views/Shared/_LoginPartial.cshtml

@using DotNetNote.Models
@inject Microsoft.Extensions.Options.IOptions<
            DotNetNote.Settings.DotNetNoteSettings> option
 
@if (User.Identity.IsAuthenticated)
{
    <form asp-controller="User" asp-action="Logout" method="post" 
        id="logoutForm" class="navbar-right">
        <ul class="nav navbar-nav navbar-right">
            @if (User.IsInRole("Users") 
                && User.FindFirst("UserId").Value == option.Value.SiteAdmin)
            {
                <li>
                    <a asp-controller="Admin" asp-action="Index">
                        <i class="fa fa-gear"></i> 대시보드
                    </a>
                </li>
            }
            <li>
                <a asp-controller="User" asp-action="UserInfor" title="Manage">
                    @User.FindFirst("UserId").Value
                </a>
            </li>
            <li>
                <button type="submit" class="btn btn-link navbar-btn navbar-link">
                    로그아웃
                </button>
            </li>
        </ul>
    </form>
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li><a asp-controller="User" asp-action="Register">회원 가입</a></li>
        <li><a asp-controller="User" asp-action="Login">로그인</a></li>
    </ul>
}

(9) 위에서 _LoginPartial.cshtml 파일을 만들었으면 이를 _Layout.cshtml 파일에 적용해 보겠습니다. Views 폴더의 Shared 폴더에 있는 _Layout.cshtml 파일을 열고 상단 메뉴 부분의 제일 오른쪽 영역에서 Html.PartialAsync() 메서드를 사용하여 _LoginPartial.cshtml 파일을 포함시킵니다. 다음 코드는 _Layout.cshtml 파일의 상단 메뉴의 전체 코드가 아닌 일부를 보여줍니다.

코드: Views/Shared/_Layout.cshtml 파일의 상단 메뉴에 로그인 정보 부분 페이지 포함

<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <a asp-area="" asp-controller="Home" 
               asp-action="Index" class="navbar-brand">닷넷노트</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a asp-area="" asp-controller="Home" 
                       asp-action="Index">Home</a></li>
                <li><a asp-area="" asp-controller="Home"
                       asp-action="About">정보</a></li>
                <li><a asp-area="" asp-controller="Home" 
                       asp-action="Contact">연락처</a></li>
                <li class="dropdown">
                    <a href="#" class="dropdown-toggle" 
                       data-toggle="dropdown">예제 <b class="caret"></b></a>
                    <ul class="dropdown-menu">
                    </ul>
                </li>
            </ul>
            @await Html.PartialAsync("_LoginPartial")
        </div>
    </div>
</div>

34.10.5 따라하기 4: 각각의 뷰 페이지 테스트

(1) DotNetNote 웹 프로젝트를 [Ctrl]+[F5]를 눌러 실행하고 /User/Index 페이지를 요청합니다. 앞서 Index 액션 메서드에 지정한 [Authorize] 특성에 의해 자동으로 로그인 페이지로 이동됩니다. 그림 34 4 로그인 페이지

(2) 회원 가입 버튼을 눌러 회원 가입 페이지로 이동합니다. 테스트를 위한 아이디와 비밀번호를 입력하고, <가입> 버튼을 눌러 저장합니다. 그림 34 5 회원 가입 페이지

(3) 다시 로그인 페이지로 이동해서 앞서 작성한 아이디와 비밀번호를 입력하고 <로그인> 버튼을 눌러 로그인을 진행합니다. 아이디와 암호가 맞지 않으면 로그인이 되지 않습니다. 그림 34 6 로그인 유효성 검사

(4) 로그인 후에는 자동으로 Index 페이지로 이동해서 다음과 같이 로그인 사용자 이름과 로그아웃 링크를 표시합니다.

그림 34 7 로그인 확인

참고: 쿠키 정보 확인 엣지 웹 브라우저에서는 F12 개발자 도구의 디버거 탭의 쿠키 영역에서 로그인 시 생성되는 쿠키 정보를 확인할 수 있습니다. 로그아웃하거나 로그인 전에는 인증 쿠키값이 생성되지 않습니다.

(5) _LogingPartial.cshtml 파일이 포함되는 레이아웃에 의해서 Home/Index 요청 시 다음과 같이 오른쪽 상단에 회원가입과 로그인 링크가 나타난다.

그림 34 8 회원 가입 및 로그인 링크 표시

(6) 로그인 링크를 클릭하여 DotNetNoteSettings.json 파일의 SiteAdmin 항목에 지정된 사용자로 로그인하면 관리자 페이지인 대시보드로 이동할 수 있는 링크를 제공합니다.

그림 34 9 사용자 정보 및 대시보드 링크 표시

34.10.6 따라하기 5: Web API에 [Authorize] 특성 적용하기

(1) ASP.NET Core Web API 컨트롤러를 만들면 바로 웹 브라우저 또는 Web API REST 클라이언트라 불리는 피들러 또는 클롬 Postman 같은 확장 도구를 사용해서 Web API를 테스트할 수 있습니다. 이때 인증되지 않은 사용자는 Web API를 사용하지 못하도록 설정하는 방법을 예제로 만들어보겠습니다. DotNetNote 프로젝트의 Controllers 폴더에 WebApiTestWithAuthorizeController.cs 파일로 Web API 컨트롤러를 생성합니다. 기본 제공 코드를 모두 제거한 후 다음과 같이 코드를 작성합니다.

***코드: ***

//  Controllers/WebApiTestWithAuthorizeController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
 
namespace DotNetNote.Controllers
{
    [Route("api/[controller]")]
    public class WebApiTestWithAuthorizeController : Controller
    {
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] {
                "[Authorize] 특성을 적용하면,"
                , "인증되지 않았을 때 로그인 페이지로 이동합니다." };
        }
    }
}

(2) 웹 브라우저 또는 Postman을 통해서 /api/WebApiTestWithAuthorize로 경로를 요청하면 다음과 같이 정상적으로 출력됩니다. 그림 34 10 Web API에 Authorize 특성 적용하기

(3) 인증된 사용자만이 Web API를 호출할 수 있도록 Web API 컨트롤러 클래스에 [Authorize] 특성을 적용합니다.

***코드: ***

//  Controllers/WebApiTestWithAuthorizeController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
 
namespace DotNetNote.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    public class WebApiTestWithAuthorizeController : Controller
    {
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] {
                "[Authorize] 특성을 적용하면,"
                , "인증되지 않았을 때 로그인 페이지로 이동합니다." };
        }
    }
}

(4) 다시 웹브라우저로 Web API 경로를 요청하면 자동으로 로그인 페이지로 리디렉션됩니다. 물론 Web API에는 더 많은 인증 방법이 있지만, [Authorize] 특성을 적용하는 방법은 인증된 사용자만 Web API에 접근하도록 하는 가장 간단한 방법입니다. 그림 34 11 인증되지 않았을 때 로그인 페이지로 이동

34.10.7 마무리

이상으로 ASP.NET Core에서의 인증 방식인 쿠키 인증 방식에 대해서 살펴보았습니다. 기타 인증 방식으로는 ASP.NET Identity와 Identity Server 등의 기술 등이 있는데 ASP.NET Core에서는 기본 웹 프로젝트 템플릿으로 ASP.NET Identity 방식의 인증이 사용됩니다. 하지만 EF(Entity Framework) Core 기술과 ASP.NET Identity 방식이 너무 밀접하게 연관되어 있어서 사용하기 쉽지 않다는 단점이 있습니다. 우선 가장 쉽고 빠르게 인증할 수 있는 쿠키 인증을 살펴보았으니 기회가 되면 ASP.NET Core의 다른 인증 방식도 살펴보기 바랍니다.

34.11 ASP.NET Core Identity - Identity 옵션 설정

코드: DotNetNote/Startup.cs

// Identity 옵션 설정
services.Configure<IdentityOptions>(options => 
{
    // 암호 설정
    options.Password.RequiredLength = 8;
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;

    // 잠금 설정
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);

    // 사용자 설정
    options.User.RequireUniqueEmail = true;
});

로그인 이름: User.Identity.Name

34.12 <추가> 참고: ASP.NET Core 세션 상태 관리

Microsoft.AspNetCore.Session NuGet 패키지는 ASP.NET 코어에서 세션 개체를 관리하는 미들웨어를 제공합니다. ASP.NET Core 응용 프로그램에서 세션을 사용하려면, 이 패키지를 NuGet의 종속성으로 추가해야합니다.

  • "Microsoft.AspNetCore.Session" : "2.2.0"
    다음 단계는 Startup 클래스에서 세션을 구성하는 것입니다. Startup 클래스의 ConfigureServices 메서드에서 AddSession() 메서드를 호출해야합니다.

***코드: ***

//  Startup.cs 파일의 ConfigureServices 메서드
#region [1] Session 개체 사용
//[0] 세션 개체 사용: Microsoft.AspNetCore.Session.dll NuGet 패키지 참조
//services.AddSession(); 
// Session 개체 사용시 옵션 부여 
services.AddSession(options =>
{
    // 세션 유지 시간
    options.IdleTimeout = TimeSpan.FromMinutes(30);
}); 
#endregion

AddSession() 메서드에는 유휴 시간 초과, 쿠키 이름 및 쿠키 도메인 등과 같은 다양한 세션 옵션을 허용하는 하나의 오버로드 메서드가 있습니다. 세션 옵션을 전달하지 않으면 시스템은 기본 옵션을 사용합니다. 이제 Startup 클래스의 Configure 메서드에서 "UseSession" 메서드를 호출해야합니다. 이 메서드는 응용 프로그램에 대한 세션을 사용 가능하게합니다.

***코드: ***

//  Startup.cs 파일의 Configure 메서드 
#region [2] TempData와 Session 개체 사용 
//[DNN] TempData 개체 사용
app.UseSession(); //[!] 세션 개체 사용, 반드시 UseMvc() 이전에 호출되어야 함 
#endregion

세션에 액세스하는 방법

HttpContext가 설치되고 구성되면 세션을 사용할 수 있습니다. 컨트롤러 클래스에서 세션을 사용하려면 컨트롤러에서 "Microsoft.AspNetCore.Http"를 참조해야합니다. Set, SetInt32 및 SetString과 같은 세션 값을 설정할 수있는 세 가지 방법이 있습니다. "Set" 메서드는 바이트 배열을 인수로 받아들입니다. SetInt32 및 SetString 메서드는 Set의 확장 메서드로 내부적으로 바이트 배열을 int 및 string으로 각각 캐스팅합니다. Get, GetInt32 및 GetString과 같이 세션에서 값을 검색하는 데 사용되는 세 가지 메서드가 있습니다. 바이트 배열을 저장하는 주된 이유는 세션 값을 원격 서버의 저장소에 대해 직렬화 할 수 있는지 확인하기 위해서입니다. int와 string은 별개로 바이트 배열로 직렬화하여 세션에 저장해야합니다.

***코드: ***

//  /Controllers/SessionDemoController.cs 
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System;

namespace DotNetNote.Controllers
{
    /// <summary>
    /// Session 개체 사용 데모 
    /// </summary>
    public class SessionDemoController : Controller
    {
        public IActionResult Index()
        {
            //[1] 세션(Session) 개체 저장
            HttpContext.Session.SetString("Username", "Green");
            return View();
        }

        public IActionResult GetSession()
        {
            //[2] 세션(Session) 개체 읽기
            ViewBag.Username = HttpContext.Session.GetString("Username"); 
            return View(); 
        }

        /// <summary>
        /// 세션에 날짜값 저장
        /// /Extensions/SessionExtensions.cs 파일이 필요
        /// </summary>
        public IActionResult SetDate()
        {
            // 현재 시간을 세션 개체에 저장
            HttpContext.Session.Set<DateTime>("NowDate", DateTime.Now);
            return RedirectToAction("GetDate");
        }

        /// <summary>
        /// 세션에서 날짜값 읽어오기
        /// </summary>
        public IActionResult GetDate()
        {
            // 세션에서 "NowDate"의 값을 읽어오기
            var date = HttpContext.Session.Get<DateTime>("NowDate");
            var sessionTime = date.TimeOfDay.ToString();
            var currentTime = DateTime.Now.TimeOfDay.ToString();
            return Content($"현재 시간: {currentTime} - "
                + $"세션에 저장되어 있는 시간: {sessionTime}");
        }
    }

    /// <summary>
    /// 개체 형식을 세션에 저장하고 읽어오고자 한다면. 
    /// https://docs.microsoft.com/ko-kr/aspnet/core/fundamentals/app-state
    /// http://www.c-sharpcorner.com/article/session-state-in-asp-net-core/
    /// </summary>
    public static class SessionExtensions
    {  
        public static T GetComplexData<T>(this ISession session, string key)
        {  
            var data = session.GetString(key);  
            if (data == null)  
            {  
                return default(T);  
            }  
            return JsonConvert.DeserializeObject<T>(data);  
        }   
      
        public static void SetComplexData(
            this ISession session, string key, object value)
        {  
            session.SetString(key, JsonConvert.SerializeObject(value));  
        }  
    }  
}

***코드: ***

//  /Views/SessionDemo/Index.cshtml
@{
    Layout = null;
}

<h1>세션 저장(페이지 로드)</h1>
<h3>현재 페이지가 로드되면 세션 개체가 저장됩니다.</h3>

<a href="/SessionDemo/GetSession">세션 확인</a>

***코드: ***

//  /Views/SessionDemo/GetSession.cshtml
@{ 
    Layout = null;
}

<h1>저장된 세션 출력</h1>

<h2>@ViewBag.Username</h2>

34.13 개체 형식을 세션에 저장하고 읽어오기

https://docs.microsoft.com/ko-kr/aspnet/core/fundamentals/app-state

DotNetNote_34_8_개체 형식을 세션에 저장하고 읽어오기.wmv

코드: DotNetNote/Extensions/SessionExtensions.cs

using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;

public static class SessionExtensions
{
    public static void Set<T>(this ISession session, string key, T value)
    {
        session.SetString(key, JsonConvert.SerializeObject(value));
    }

    public static T Get<T>(this ISession session, string key)
    {
        var value = session.GetString(key);
        return value == null ? default(T) :
                              JsonConvert.DeserializeObject<T>(value);
    }
}

***코드: ***

//  DotNetNote/Controllers/SessionDemoController.cs 컨트롤러에 2개 액션 메서드 추가
/// <summary>
/// 세션에 날짜값 저장
/// /Extensions/SessionExtensions.cs 파일이 필요
/// </summary>
public IActionResult SetDate()
{
    // 현재 시간을 세션 개체에 저장
    HttpContext.Session.Set<DateTime>("NowDate", DateTime.Now);
    return RedirectToAction("GetDate");
}

/// <summary>
/// 세션에서 날짜값 읽어오기
/// </summary>
public IActionResult GetDate()
{
    // 세션에서 "NowDate"의 값을 읽어오기
    var date = HttpContext.Session.Get<DateTime>("NowDate");
    var sessionTime = date.TimeOfDay.ToString();
    var currentTime = DateTime.Now.TimeOfDay.ToString();
    return Content($"현재 시간: {currentTime} - "
        + $"세션에 저장되어 있는 시간: {sessionTime}");
}

34.15 세션 정보를 인-메모리가 아닌 SQL Server에 저장하기

세션 정보를 인-메모리가 아닌 SQL Server에 저장하여 사용할 수 있습니다. 절차는 간단합니다. 세션 정보를 담을 데이터베이스를 하나 준비하고 이곳에 SQLSessions 테이블과 이에 대한 인덱스를 구성합니다.

***코드: ***

//  DotNetNote.Database 프로젝트: SqlSessions.sql 
-- SQL Server를 사용하여 세션 개체를 저장하고자 할 때 사용되는 테이블 구조
CREATE TABLE [dbo].[SQLSessions](  
    [Id] [nvarchar](449) NOT NULL,  
    [Value] [varbinary](max) NOT NULL,  
    [ExpiresAtTime] [datetimeoffset](7) NOT NULL,  
    [SlidingExpirationInSeconds] [bigint] NULL,  
    [AbsoluteExpiration] [datetimeoffset](7) NULL,  
 CONSTRAINT [pk_Id] PRIMARY KEY CLUSTERED   
(  
    [Id] ASC  
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]  
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]  
GO

-- SQL Server를 사용하여 세션 개체를 저장하고자 할 때 사용되는 인덱스 구조
CREATE NONCLUSTERED INDEX [Index_ExpiresAtTime] ON [dbo].[SQLSessions]  
(  
    [ExpiresAtTime] ASC  
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)  
GO  

데이터베이스와 테이블이 준비가 되었으면 ASP.NET Core 프로젝트의 Startup.cs 파일의 ConfigureServices 메서드에 아래 코드와 같이 기록을 합니다.

***코드: ***

//  Startup.cs
public void ConfigureServices(IServiceCollection services)
{

    //<세션 정보를 SQL Server에 저장하기 위한 절차>
    services.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = Configuration["ConnectionString"];
        options.SchemaName = "dbo";
        options.TableName = "SQLSessions";                            
    });
    //</세션 정보를 SQL Server에 저장하기 위한 절차>

위 2단계를 거치면, 세션 개체가 사용되면 세션에 대한 정보는 SQL Server의 테이블에 저장이 됩니다.

34.16 ASP.NET Core Web API에서 JSON Web Token 사용하기

34.16.1 JSON Web Token 소개

Blazor, Angular, React, Vue, jQuery와 같은 JavaScript 또는 Windows Forms, Xamarin과 같은 데스크톱 또는 모바일 앱에서 로그인, 로그아웃과 같은 인증 기능을 구현하고자할 때 사용할 수 있는 기술이 JSON Web Token입니다. 줄여서 JWT라고 부릅니다.

34.16.1.1 세션, 쿠키, 토큰 인증(JWT)

ASP.NET에서 사용할 수 있는 인증(Authentication) 방식은 대표적으로 세션 인증, 쿠키 인증 그리고 이번에 살펴볼 토큰 인증이 있습니다.

  • 세션 인증(Session)
  • 쿠키 인증(Cookies)
  • 토큰 인증(JWT)

34.16.1.2 JWT 인증을 위한 NuGet 패키지

JWT 인증을 구현하려면 프로젝트에 2개의 NuGet 패키지가 추가되어야 합니다.

  • JWT 토큰
    • PM> dotnet add package System.IdentityModel.Tokens.Jwt
  • JWT Bearer 토큰
    • PM> dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

34.16.1.3 토큰 생성 샘플 코드

토큰을 생성하는 가장 기본적인 샘플 코드는 다음과 같습니다.

***코드: ***

// 
var jwt = new JwtSecurityToken(); 
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); 

public class JwtPacket
{
    public string Token { get; set; }
}

return new JwtPacket() { Token = encodedJwt };

34.16.2 JWT 인증 따라하기

(1) 프로젝트를 생성합니다. ASP.NET Core Angular 템플릿 DotNetNoteCom http://www.dotnetnote.com http://angular.dul.me

(2) NuGet 패키지를 추가합니다.

  • JWT 토큰 패키지 추가
    • System.IdentityModel.Tokens.Jwt.dll 추가
  • JWT Bearer 토큰 체크 패키지 추가
    • Microsoft.AspNetCore.Authentication.JwtBearer.dll 추가

(3) Startup.cs 파일을 열고 JWT 관련 미들웨어를 추가합니다. 자세한 코드는 다음 순서를 참고합니다.

  • ConfigureServices() 메서드
    • services.AddAuthentication().AddJwtBearer();
  • Configure() 메서드
    • app.UseAuthentication();

(4) AddJwtBearer() 메서드의 세부 옵션 설정 값은 다음과 같습니다. 보안키가 들어가는 GetBytes() 메서드의 ____ 부분은 반드시 긴 문자열로 설정해야 합니다. 이 보안키는 따로 appsettings.json과 같은 곳에 보관하면됩니다.

***코드: ***

//  Startup.cs 파일의 인증 관련 기본 코드 샘플 
#region ASP.NET Core 쿠키 인증: ConfigureServices()
////[1] ASP.NET Core 쿠키 인증: 단순형
//services.AddAuthentication("Cookies").AddCookie(options =>
//{
//    options.LoginPath = "/User/Login/";
//    options.AccessDeniedPath = "/User/Forbidden/";
//});

// _httpContextAccessor.IsAuthenticated 등 사용 
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); 

//[!] ASP.NET Core 쿠키 인증 및 JWT 인증: 기본형
//services.AddAuthentication()
//services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = 
        CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = 
        CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = 
        CookieAuthenticationDefaults.AuthenticationScheme;
})
//services.AddAuthentication(options =>
//{
//    options.DefaultAuthenticateScheme =
//        JwtBearerDefaults.AuthenticationScheme;
//    options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
//    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
//})
.AddCookie(options =>
{
    options.LoginPath = new PathString("/User/Login");
    options.LogoutPath = "/User/Logout";
    options.AccessDeniedPath = new PathString("/User/Forbidden");
    //options.Cookie.Name = "Cookies"; // 직접 이름 지정
    options.Cookie.Name = 
        CookieAuthenticationDefaults.AuthenticationScheme; // 정의되어 있는 이름으로 지정
    options.Cookie.HttpOnly = false;
    options.Cookie.SameSite = SameSiteMode.None; 
    options.SlidingExpiration = true;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = false,
        ValidateIssuer = false,
        ValidateIssuerSigningKey = true,
        // 보안키 문자열 길게 설정할 것
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(
                Configuration["SymmetricSecurityKey"])),
        ValidateLifetime = true,
        ClockSkew = TimeSpan.FromMinutes(5)
    };
});
#endregion

(5) 회원 관련 모델 클래스인 SignBase.cs, SignViewModel.cs 모델 클래스를 생성합니다. SignBase.cs 클래스 생성 SignBases.sql : 최소 => 강의 ProductBase.sql Signs.sql : 최대 => 제품/운영 Products.sql SignViewModel.cs 클래스 생성 UserViewModel.cs LoginViewModel.cs

(6) ISignRepository.cs 인터페이스와 SignRepository.cs 리포지토리 클래스를 생성합니다. 생략...

(7) SignServicesController.cs 이름으로 Web API를 생성합니다. Login 메서드 구현 POST: /api/SignServices/Login

    [HttpPost("Login")]
    public IActionResult Login([FromBody] SignViewModel model)
    {
        
        return Ok();
    }

LoginTest 메서드 구현
	GET: /api/SignServices/LoginTest

    [HttpGet("LoginTest")]
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    public IActionResult LoginTest()
    {
        return Ok("인증된 사용자만 보는 내용");
    }

(8) 토큰 발행 공식 코드

Login 메서드 구현
	POST: /api/SignServices/Login
		일단은 무조건 로그인 통과 => 토큰 발행
	기본 토큰: 
		{

"token": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.e30." }

[HttpPost("Login")] public IActionResult Login([FromBody] SignViewModel model) { if (!IsLogin(model)) { return NotFound("이메일 또는 암호가 틀립니다."); }

//[1] 보안키 생성
var securityKey = 
    new SymmetricSecurityKey(Encoding.UTF8.GetBytes("DotNetNote1234567890"));
var signingCredentials = 
    new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

//[2] 클레임 생성
var claims = new Claim[]
{
    new Claim(JwtRegisteredClaimNames.Sub, model.Email)
};

//[3] 토큰 생성하기
var token = new JwtSecurityToken(
    claims: claims, 
    signingCredentials: signingCredentials, 
    expires: DateTime.Now.AddMinutes(5));
var t = new JwtSecurityTokenHandler().WriteToken(token);

return Ok(new { Token = t });

}

(9) [Authorize] 특성으로 테스트 LoginTest 메서드 테스트 GET: /api/SignServices/LoginTest 접근 시도 로그인 실패 로그인 성공

(10) 토큰 확인: https://jwt.io/ 사이트

{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhQGEuY29tIiwiZXhwIjoxNTIzNDE4NDIwfQ.8qLixvnvS4Zsky3pOEx5M-vWo0RoDxtV3EANh6kHnUc" }

(11) 토큰 발행시 사용할 키 값들은 appsettings.json 파일에 보관해 놓으면 관리하기 편리합니다.

34.16.2.1 JWT 토큰 생성 및 전송

JWT 토큰을 생성하고 API 요청에서 전송하는 방법은 다음과 같습니다.

  1. JWT 토큰 생성을 위한 헤더와 페이로드를 JSON 형식으로 작성합니다. 헤더와 페이로드는 Base64로 인코딩됩니다.
// Header
var header = new { alg = "HS256", typ = "JWT" };
var headerSerialized = JsonConvert.SerializeObject(header);
var headerBase64 = Base64UrlEncoder.Encode(headerSerialized);

// Payload
var payload = new { sub = "12345", name = "John Doe", exp = 1300819380 };
var payloadSerialized = JsonConvert.SerializeObject(payload);
var payloadBase64 = Base64UrlEncoder.Encode(payloadSerialized);

// Signature
var bytesToSign = Encoding.UTF8.GetBytes(headerBase64 + "." + payloadBase64);
var secret = Encoding.UTF8.GetBytes("mysecretkey");
var signature = Base64UrlEncoder.Encode(new HMACSHA256(secret).ComputeHash(bytesToSign));

// JWT Token
var jwtToken = headerBase64 + "." + payloadBase64 + "." + signature;
  1. Authorization 헤더에 생성된 JWT 토큰을 포함하여 API 요청을 보냅니다.
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);

34.16.2.2 JWT 토큰 검증

JWT 토큰의 유효성을 검증하는 방법은 다음과 같습니다.

  1. JWT.IO 사이트에 접속하여 검증 탭으로 이동합니다.
  2. 검증하려는 JWT 토큰을 복사하여 텍스트 상자에 붙여넣습니다.
  3. 검증 결과를 확인합니다.

또는 C# 코드로 JWT 토큰을 검증할 수도 있습니다.

public static bool IsTokenValid(string token, string secretKey)
{
    var parts = token.Split('.');
    var headerJson = Encoding.UTF8.GetString(Base64UrlEncoder.Decode(parts[0]));
    var payloadJson = Encoding.UTF8.GetString(Base64UrlEncoder.Decode(parts[1]));

    var headerData = JsonConvert.DeserializeObject<Dictionary<string, object>>(headerJson);
    var payloadData = JsonConvert.DeserializeObject<Dictionary<string, object>>(payloadJson);

    if (!headerData["alg"].Equals("HS256"))
    {
        return false;
    }

    var bytesToSign = Encoding.UTF8.GetBytes(parts[0] + "." + parts[1]);
    var secret = Encoding.UTF8.GetBytes(secretKey);

    using (var sha256 = new HMACSHA256(secret))
    {
        var hash = sha256.ComputeHash(bytesToSign);
        var signature = Base64UrlEncoder.Encode(hash);

        return signature.Equals(parts[2]);
    }
}

34.16.2.3 인증된 사용자 클레임 정보 조회

인증된 사용자의 클레임 정보를 조회하는 방법은 다음과 같습니다.

var identity = HttpContext.User.Identity as ClaimsIdentity;
var userId = identity.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var userName = identity.FindFirst(ClaimTypes.Name)?.Value;
var userEmail = identity.FindFirst(ClaimTypes.Email)?.Value;

***코드: ***

// 
[HttpGet("LoginTest")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult LoginTest()
{
    //return Ok("인증된 사용자만 보는 내용");
    return Ok(HttpContext.User.Claims.First().Value); // a@a.com
}

34.16.3 마무리

JWT를 사용하여 토큰을 발행하는 내용을 살펴보았습니다. 자세한 소스에 대한 분석은 DotNetNote 프로젝트의 앵귤러 파트의 sign 모듈을 분석해보겠습니다.

ASP.NET Core에서 Same Site Cookies를 사용한 인증 및 권한 관리

ASP.NET Core에서 인증 및 권한 관리를 위한 Same Site Cookies를 사용하는 방법에 대해 알아봅시다.

Same Site Cookies란?

Same Site Cookies는 웹 사이트 간 요청 위조(CSRF) 공격을 방지하기 위해 도입된 쿠키 속성입니다. 이 속성은 브라우저가 다른 사이트에서 요청을 보낼 때 쿠키를 함께 전송하지 않도록 설정할 수 있습니다.

Same Site Cookies 설정 방법

  1. Startup.cs 파일을 열고, ConfigureServices 메서드에 다음 코드를 추가합니다.
services.Configure<CookiePolicyOptions>(options =>
{
    options.MinimumSameSitePolicy = SameSiteMode.Strict;
    options.HttpOnly = HttpOnlyPolicy.Always;
    options.Secure = CookieSecurePolicy.Always;
});
  1. Configure 메서드에 다음 코드를 추가하여 쿠키 정책을 사용하도록 설정합니다.
app.UseCookiePolicy();
  1. 인증 및 권한 관리를 위해 Identity를 사용한다면, Startup.cs 파일의 ConfigureServices 메서드에 다음 코드를 추가합니다.
services.ConfigureApplicationCookie(options =>
{
    options.Cookie.SameSite = SameSiteMode.Strict;
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});

위의 설정을 사용하면, ASP.NET Core 애플리케이션에서 Same Site Cookies를 사용하여 인증 및 권한 관리를 보다 안전하게 수행할 수 있습니다.

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com