ASP.NET Core 인증과 권한

  • 111 minutes to read

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

인증과 권한 관련 미리보기 강좌는 다음 경로의 강좌를 먼저 보신 후 진행하세요.

ASP.NET Core 8.0 인증 및 권한 부여의 완전 가이드

DotNetNote 프로젝트와 Hawaso 프로젝트

ASP.NET Core에서 주로 사용하는 인증 기술은 크게 4가지 기술이 있습니다.

  1. 쿠키 인증
  2. ASP.NET Core Identity 인증
  3. 토큰 인증(JWT)
  4. OAuth2 / OpenID Connect

ASP.NET Core를 다루는 기술 책에서 사용한 데모 소스인 DotNetNote 프로젝트는 가장 기본이 되는 쿠키 인증을 사용하고, Blazor Server 강의 데모 소스인 Hawaso 프로젝트에서는 ASP.NET Core Identity 인증 기반 쿠키 인증을 사용합니다.

DotNetNote 프로젝트는 단 하나의 'Admin' 역할을 지정하는 방식을 사용하며, Hawaso 프로젝트에서는 역할 기반 인증을 사용하여 여러 사용자에 대해서 'Aministrators' 역할에 포함시켜 웹 애플리케이션을 확장하는 방식을 사용합니다.

만약, 아직 배우지 않은 역할 기반 인증을 사용하는 소스를 참고하려면 위 경로의 Hawaso 프로젝트를 참고하기 바랍니다.

즉, DotNetNote 프로젝트는 처음 설계가 단일 관리자를 목표로 했기에 다중 관리자를 사용하려면 아예 ASP.NET Core Identity 인증을 사용하는 Hawaso 프로젝트의 소스를 참고하세요.

ASP.NET Core와 Blazor Server를 활용하여 역할 기반 인증 시스템을 구축하는 과정은 일반적인 교재나 강의에서는 자주 다루지 않는 복잡한 주제입니다. 이 고급 주제에는 특정 페이지에 대한 접근 권한을 설정하는 등의 세밀한 기능이 포함됩니다. 만약 여러분이 이러한 역할 기반의 접근 권한 설정 및 관련된 고급 기능들을 깊이 있게 배우고 싶다면, VisualAcademy의 유튜브 채널에서 제공하는 VIP 멤버십 프로그램을 활용하는 것을 추천합니다. 이 프로그램은 심도 있는 학습과 실질적인 지식을 제공하여, 여러분이 ASP.NET Core와 Blazor Server를 사용하여 보다 효율적이고 전문적인 웹 애플리케이션을 개발하는 데 필요한 기술을 습득할 수 있도록 도와줍니다.

1. ASP.NET Core Identity

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

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

2. 회원 관리의 주요 범위

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

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

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

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

  • Login
  • Profile
  • Register
  • Logout

ASP.NET Core 기본 제공 회원 인증 UI

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

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

업무적 관점에서 인증과 권한은 다음과 같이 표현될 수 있습니다.

  • 계정 관리(Authentication)
    • 회원 관리
  • 접근 통제(Authorization)
    • 역할 관리
  • 게시판 접근 허가(Permission)
    • 인증된 사용자에게 특정 게시판(페이지)에 대한 사용 허가 부여

인증과 권한 요약

인증: Authentication

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

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

권한(허가): 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): 다중 인증 요소를 사용하여 보안 강화

4 ASP.NET Core 인증

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

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

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

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

5 ASP.NET Core의 인증 옵션

템플릿 제공

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 웹 서버가 필요하고 로컬 로그인한 사용자 기반 인증입니다.

소셜 인증

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

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

ASP.NET Core Identity

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

ASP.NET Core Identity

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

Identity 뜻

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

IdentityServer

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

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

  • Microsoft.AspNetCore.Authentication.Cookies.dll

쿠키 인증 코드 예시

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

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

쿠키 인증을 위한 코드 조각은 다음과 같습니다.

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

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

위 코드에서 "Cookies"와 CookieAuthenticationDefaults.AuthenticationScheme는 동일합니다.

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

[Authorize]

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

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

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

CookieAuthenticationDefaults.AuthenticationScheme

CookieAuthenticationDefaults.AuthenticationScheme은 ASP.NET Core 애플리케이션에서 쿠키 기반 인증을 구성할 때 사용하는 기본 인증 체계의 이름을 정의하는 상수입니다. 이 값은 Cookie Authentication을 구성하고 사용할 때 인증 체계를 식별하는 데 사용됩니다.

쿠키 기반 인증은 사용자가 웹 응용 프로그램에 로그인할 때 사용자 정보를 암호화하여 클라이언트 측에 쿠키로 저장하고, 이 쿠키를 사용하여 사용자의 세션을 추적하고 인증 상태를 유지하는 방법 중 하나입니다. AuthenticationScheme 상수는 이러한 인증 체계를 정의하고 구성할 때 사용자 정의하는 데 필요한 키 역할을 합니다.

일반적으로 AuthenticationScheme 값은 "Cookies" 또는 "Identity.Cookies"와 같이 문자열 상수로 설정됩니다. 이 값은 Cookie Authentication의 기본 구성에서 사용됩니다. 예를 들어:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Login";
        options.AccessDeniedPath = "/Account/AccessDenied";
    });

위의 코드에서 CookieAuthenticationDefaults.AuthenticationScheme을 사용하여 Cookie Authentication을 설정하고 인증 체계를 "Cookies"로 지정합니다. 이렇게 설정된 체계는 로그인 경로, 권한 없음 경로 등과 연관된 기본 설정을 사용합니다.

AuthenticationScheme 값을 사용자 정의하려면, 필요에 따라 사용자 고유의 이름을 할당할 수 있으며, 이를 통해 여러 인증 체계를 구성하고 관리할 수 있습니다.

ASP.NET Core 8.0 쿠키 인증 설정

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

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

var builder = WebApplication.CreateBuilder(args);

// [!] ASP.NET Core 8.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 8.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;
});

ASP.NET Core에서의 [Authorize] 특성

ASP.NET Core에서 [Authorize] 특성은 웹 응용 프로그램의 컨트롤러 또는 액션 메서드에 적용되는 속성입니다. 이 특성은 사용자 인증 및 권한 부여를 관리하기 위해 사용됩니다. [Authorize] 특성을 사용하면 특정 컨트롤러 또는 액션 메서드에 액세스 권한이 있는 사용자만 해당 리소스에 접근할 수 있도록 제한할 수 있습니다.

주요 기능 및 사용법

  1. 인증(Authentication): [Authorize] 특성을 적용한 컨트롤러 또는 액션 메서드에 액세스하려는 사용자는 먼저 인증되어야 합니다. 인증된 사용자는 로그인한 사용자로 식별됩니다.

  2. 권한 부여(Authorization): [Authorize] 특성은 인증된 사용자에게 특정 권한을 부여할 수 있습니다. 이렇게 하면 권한이 없는 사용자는 접근을 거부합니다. 예를 들어, [Authorize(Roles = "Administrators")]와 같이 특정 역할을 가진 사용자만 접근할 수 있도록 설정할 수 있습니다.

  3. 사용자 지정 규칙(Custom Policies): [Authorize] 특성은 미리 정의된 권한 외에도 사용자 지정 권한 검사를 수행할 수 있습니다. 사용자 지정 규칙을 만들어 [Authorize] 특성에 적용할 수 있으며, 커스텀 규칙에 따라 액세스를 제한할 수 있습니다.

예를 들어, 다음은 [Authorize] 특성을 사용하여 컨트롤러 액션 메서드에 인증 및 권한 부여를 설정하는 예제입니다:

[Authorize] // 모든 인증된 사용자에게 접근 허용
public class MyController : Controller
{
    [Authorize(Roles = "Administrators")] // "Administrators" 역할을 가진 사용자에게만 접근 허용
    public IActionResult AdminOnlyAction()
    {
        // ...
    }

    [Authorize(Policy = "CustomPolicy")] // 사용자 지정 규칙에 따라 접근 허용
    public IActionResult CustomPolicyAction()
    {
        // ...
    }
}

[Authorize] 특성과 [AllowAnonymous] 특성

Microsoft.AspNetCore.Authorization 네임스페이스에 있는 Authorize 특성은 인증되지 않은 사용자의 접근을 거부하도록 컨트롤러 클래스 및 액션 메서드에 지정할 수 있습니다. 이를 통해 웹 응용 프로그램의 특정 부분에 보안을 적용할 수 있습니다.

@attribute

  • 코드 비하인드가 없는 .razor 파일에 [Authorize] 특성을 적용하고자 할 때 사용됩니다.
  • 페이지 상단에 @attribute 디렉티브를 사용하여 [Authorize] 특성을 지정하면 해당 페이지에 접근하려는 사용자는 반드시 인증되어야 합니다. 인증되지 않은 사용자는 해당 페이지에 액세스할 수 없습니다.

[AllowAnonymous] 특성

[AllowAnonymous] 특성은 특정 컴포넌트, 컨트롤러, 또는 액션 메서드에 대한 인증 없이 모든 사용자가 접근할 수 있도록 설정하고자 할 때 사용됩니다. 이 특성을 사용하면 특정한 인증 또는 권한 검사 없이 누구나 해당 리소스에 접근할 수 있게 됩니다.

예를 들어, 다음은 [AllowAnonymous] 특성을 사용하여 특정 컨트롤러의 액션 메서드에 대한 접근을 인증 없이 허용하는 예제입니다:

[AllowAnonymous] // 모든 사용자에게 접근 허용
public IActionResult PublicAction()
{
    // ...
}

8. 쿠키 인증을 위한 주요 명령어 개요

쿠키 인증과 관련된 주요 명령어들을 정리하여, 이들의 사용 방법에 대해 알아보겠습니다. 여기에는 간략한 코드 조각과 함께 실습을 통해 전체 코드를 이해할 기회를 제공합니다. 이 예시들은 UserController.cs 파일에 아직 구현되지 않은 예제 코드로, ASP.NET Core 쿠키 인증의 구현 방법을 설명하기 위해 사용됩니다.

ClaimsPrincipal 클래스의 역할

ASP.NET Core에서 쿠키 인증은 주로 ClaimsPrincipal 클래스를 기반으로 작동합니다.

public virtual ClaimsPrincipal User { get; }

클레임 기반 인증(Claims-Based Authentication)의 이해

클레임(Claim)은 사용자 아이덴터티에 대한 속성을 이름과 값의 쌍으로 나타냅니다. 하나의 아이덴터티는 다수의 클레임을 포함할 수 있습니다.

  • ClaimsIdentity 클래스는 하나의 아이덴터티와 이에 연관된 클레임들을 표현합니다.
  • ClaimsPrincipal 클래스는 하나 이상의 아이덴터티(Identity)를 가진 사용자를 나타냅니다.

Program.cs 내의 ConfigureServices() 메서드

쿠키 인증을 설정하기 위해 ConfigureServices 메서드에 다음과 같은 코드를 추가합니다.

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

또는

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

Program.cs 내의 Configure() 메서드

애플리케이션의 미들웨어 파이프라인에 인증과 권한 부여 미들웨어를 추가하기 위해 Configure 메서드에 다음과 같은 코드를 사용합니다.

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

이러한 설정들은 쿠키 기반 인증 시스템을 구축하는 데 필수적인 요소로, ASP.NET Core 애플리케이션의 보안을 강화하는 데 중요한 역할을 합니다.

로그인

로그인 관련 처리는 컨트롤러 클래스의 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));

CookieAuthenticationDefaults.AuthenticationScheme는 ASP.NET Core에서 쿠키 인증을 위한 기본 인증 스키마 이름("Cookies")을 지정하는 상수입니다.

HttpContext.SignInAsync() 메서드는 현재 사용자에 대해 인증 쿠키를 생성하고 로그인 상태로 설정하는 데 사용됩니다.

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

ClaimTypes 사용

ASP.NET Core에서는 ClaimTypes 클래스를 사용하여 더 명확하고 표준화된 클레임 유형을 정의할 수 있습니다. 이 클래스에는 다양한 클레임의 유형을 나타내는 미리 정의된 상수가 포함되어 있어, 보안 주체의 신원과 속성을 더 명확하게 표현할 수 있습니다. 예를 들어, 사용자의 이름, 역할, 이메일 등을 표준 클레임 유형으로 추가할 수 있습니다.

코드: ClaimTypes를 사용한 Login() 액션 메서드의 코드 일부

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

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

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

ClaimTypes 클래스를 사용함으로써, 클레임의 의미와 목적이 더 분명해지고, 표준화된 방식으로 인증 및 권한 부여를 구현하는 데 도움이 됩니다. 이렇게 표준화된 클레임을 사용하면, 다른 시스템과의 통합이나 확장성 측면에서도 이점이 있습니다.

HttpContext 클래스

ASP.NET Core 웹 애플리케이션에서 HttpContext 클래스는 사용자 인증권한 관리를 위한 기본 제공 클래스입니다. 이를 통해 현재 요청과 사용자에 대한 정보를 관리하며, 인증 및 권한 부여와 관련된 다양한 작업을 수행할 수 있습니다.

주요 인증 관련 속성과 메서드는 다음과 같습니다:

  1. HttpContext.User
    User 속성을 통해 현재 요청을 보낸 사용자에 대한 정보를 가져올 수 있습니다. 이 개체는 사용자의 클레임을 포함하며, 사용자의 식별 정보와 권한을 나타냅니다.

  2. HttpContext.AuthenticateAsync()
    이 메서드는 인증을 수행하여 사용자를 인증된 상태로 만듭니다. 예를 들어, 사용자가 로그인할 때 이 메서드를 호출하여 쿠키 또는 토큰을 검증합니다. 인증이 완료되면 사용자의 권한을 확인하고 보호된 리소스에 접근할 수 있도록 합니다.

  3. HttpContext.ChallengeAsync()
    이 메서드는 인증되지 않은 사용자를 로그인 페이지로 리디렉션하여 인증을 유도합니다. 로그인 시도를 요구할 때 사용되며, 주로 401 상태 코드와 함께 사용자가 인증되지 않았음을 알립니다.

  4. HttpContext.ForbidAsync()
    사용자가 접근할 권한이 없는 리소스를 요청할 때 이 메서드가 사용됩니다. 403 상태 코드를 반환하며, 사용자를 Access Denied 페이지로 리디렉션합니다.

  5. HttpContext.SignInAsync()
    사용자가 로그인한 후 호출되어 인증 쿠키를 생성하고 로그인 상태를 유지합니다. 이 메서드를 통해 사용자가 로그인된 상태로 웹 애플리케이션을 이용할 수 있게 합니다.

  6. HttpContext.SignOutAsync()
    사용자를 로그아웃 상태로 만들고 인증 쿠키를 삭제합니다. 로그아웃 시 이 메서드를 호출하여 사용자의 인증 상태를 해제합니다.

HttpContext 클래스를 활용하면 사용자의 인증 및 권한 상태를 효과적으로 확인하고 관리할 수 있습니다. 이를 통해 웹 애플리케이션의 보안권한 부여 작업을 보다 체계적으로 수행할 수 있습니다.

로그인(쿠키 인증)

HttpContext.SignInAsync(ClaimsPrincipal…);

위 명령어를 실행하면 웹 브라우저에 쿠키가 생성됩니다. 주요 옵션은 다음과 같습니다.

await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme, 
    new ClaimsPrincipal(ci), 
    authenticationProperties
);

로그인 코드 조각

다음은 DotNetNote 프로젝트의 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 쿠키 인증 코드 조각

로그인 확인 및 로그인 이름 표시(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 

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 인증으로 검색해보세요.

로그아웃

ASP.NET Core에서 로그아웃 과정은 사용자의 인증 상태를 안전하고 효과적으로 해제하는 중요한 단계입니다. 이를 위해 HttpContext.SignOutAsync(); 메서드가 사용되며, 이는 인증된 사용자의 쿠키를 삭제하여 세션을 종료합니다. 최신 ASP.NET Core 버전에서 이 메서드는 인증 처리의 표준이 되었으며, Microsoft.AspNetCore.Authentication.Cookies 네임스페이스 하에 있는 CookieAuthenticationDefaults.AuthenticationScheme를 사용합니다.

로그아웃 프로세스의 안전성 강화: LocalRedirect 메서드

로그아웃 이후의 사용자 경험을 관리하는 것은 웹 애플리케이션의 보안과 사용자 인터페이스의 직관성에 매우 중요합니다. 이를 위해 LocalRedirect 메서드의 사용이 권장됩니다. LocalRedirect 메서드는 로그아웃 후 사용자를 애플리케이션 내의 안전한 페이지로 리디렉션하는 데 사용됩니다. 이 방법은 외부 URL로의 리디렉션을 방지하여 Open Redirect 공격과 같은 보안 위협으로부터 애플리케이션을 보호합니다.

예를 들어, LocalRedirect(Url.Content("~/")); 코드는 사용자를 애플리케이션의 홈 페이지로 안전하게 이동시킵니다. 이는 로그아웃 프로세스가 완료된 후 사용자가 애플리케이션의 다른 부분으로 자연스럽게 이동할 수 있도록 도와주며, 동시에 애플리케이션의 보안을 강화합니다.

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

로그아웃을 처리하는 공식적이고 안전한 방법은 아래와 같습니다.

코드: UserController.cs - Logout() 액션 메서드의 코드 일부

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

또는, 로그아웃 후 특정 액션으로 리디렉션하는 방법도 흔히 사용됩니다.

await HttpContext.SignOutAsync();
return RedirectToAction("Index"); 

이 코드 조각은 사용자가 로그아웃하고 나서 지정된 페이지나 액션으로 안전하게 리디렉션되도록 보장합니다. 이러한 방식은 사용자 경험과 웹 사이트의 보안 모두를 강화하는 효과적인 방법입니다.

쿠키와 토큰

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

인증 확인

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

***코드: ***

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

    return View(); 
}

34.8.7 참고: 챌린지 코드 조각

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

코드: 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 사용자만 볼 수 있음";
        }
    }
}

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

소개

현재 실습에 대한 소스는 다음 경로에 있습니다.

https://github.com/VisualAcademy/AspNetCoreCookieAuthenticationTest

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

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

(2) Program.cs 또는 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!");
                });
            });
        }
    }
}

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

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

코드: Startup.cs

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) 로그아웃 페이지를 요청하면 생성되었던 쿠키가 제거됩니다.

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

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

코드: Startup.cs

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) 로그인 기능 처리할 때 좀 더 다양한 클레임을 주거나, 추가적인 인증 옵션을 적용할 수 있습니다.

코드: Startup.cs

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("로그인되었습니다.");
}

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

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

코드: Program.cs

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. 마무리

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

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 테이블 생성
-- ASP.NET Web Forms, ASP.NET Core 초간단 회원 관리 강의에서 사용한 자료 

--[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 파일에 기록되어 있습니다.

코드: 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)
                    ); 
    });

    
}

따라하기 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
    {
        /// <summary>
        /// UID
        /// </summary>
        public int Id { get; set; }

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

        /// <summary>
        /// Password
        /// </summary>
        [Display(Name = "비밀번호")]
        [Required(ErrorMessage = "비밀번호를 입력하시오.")]
        [StringLength(20, MinimumLength = 6,
            ErrorMessage = "비밀번호는 6자 이상 20자 이하로 입력하시오.")]
        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(string connectionString)
        {
            con = new SqlConnection(connectionString);
        }

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

DotNetNote 프로젝트의 Program.cs 파일에는 다음 코드 조각으로 적용되어 있습니다.

services.AddSingleton<IUserRepository>(new UserRepository(Configuration.GetConnectionString("DefaultConnection")));

따라하기 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>

따라하기 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 인증되지 않았을 때 로그인 페이지로 이동

마무리

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

Unauthorized() 메서드

ASP.NET Core MVC에서 Unauthorized() 메서드는 인증되지 않은 요청에 대해 401 상태 코드를 반환하는 데 사용됩니다. 이 메서드는 주로 컨트롤러 액션 메서드에서 사용되며, 클라이언트가 인증되지 않았거나 적절한 자격 증명이 없을 때 호출됩니다.

사용 예제

컨트롤러에서 Unauthorized() 메서드를 사용하는 기본적인 예제는 다음과 같습니다.

using Microsoft.AspNetCore.Mvc;

public class AccountController : Controller
{
    public IActionResult Login(string username, string password)
    {
        if (IsValidUser(username, password))
        {
            // 유효한 사용자라면 다른 동작 수행
            return RedirectToAction("Index", "Home");
        }
        else
        {
            // 유효하지 않은 사용자일 경우 Unauthorized() 반환
            return Unauthorized();
        }
    }

    private bool IsValidUser(string username, string password)
    {
        // 사용자 유효성 검사 로직 (예: 데이터베이스 조회)
        return false; // 예시로 false 반환
    }
}

설명

  • Unauthorized() 메서드: 이 메서드는 401 Unauthorized 상태 코드를 반환합니다. 클라이언트는 인증되지 않았으며, 인증 정보를 제공해야 함을 나타냅니다.
  • IsValidUser 메서드: 실제 사용자 인증 로직을 수행하는 메서드입니다. 예제에서는 간단히 false를 반환하여 항상 인증 실패를 시뮬레이트합니다.

실습 팁

  • JWT 인증: JWT 토큰을 사용한 인증 시, 유효하지 않은 토큰이나 만료된 토큰을 받았을 때 Unauthorized()를 반환할 수 있습니다.
  • 필터 사용: 인증 필터를 사용하여 전역적으로 또는 특정 컨트롤러/액션에 대해 인증 요구 사항을 적용할 수 있습니다.

추가 정보

  • 401 Unauthorized 상태 코드: HTTP 표준 상태 코드 중 하나로, 클라이언트가 요청한 리소스에 접근할 권한이 없음을 나타냅니다. 주로 인증 정보가 없거나 잘못된 경우에 사용됩니다.
  • [Authorize] 특성: MVC에서 인증을 요구하는 특성으로, 특정 컨트롤러나 액션에 대해 인증된 사용자만 접근할 수 있도록 설정할 수 있습니다. Unauthorized() 메서드는 이 특성과 함께 사용될 수 있습니다.

[!INCLUDE 글로벌 필터로 [Authorize] 특성 적용하기]

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

Authorization

ASP.NET Core Authorization – Policy, Requirement, Handler

ASP.NET Core에서는 **정책 기반 권한 부여(Policy-based Authorization)**를 사용하여 권한을 세부적으로 관리할 수 있습니다. 이 아티클에서는 정책(Policy), 요구 사항(Requirement), 핸들러(Handler)를 정의하고 사용하는 방법에 대해 설명합니다.

Policy (정책)

**정책(Policy)**는 특정 요청에 대한 권한 부여 조건을 정의합니다. 정책에는 여러 요구 사항(Requirement)이 포함될 수 있으며, 사용자는 해당 정책을 만족해야 권한이 부여됩니다.

정책 정의

AddAuthorization() 메서드를 사용하여 정책을 정의할 수 있습니다. 예를 들어, **"성인 인증"**이라는 정책을 정의한다고 가정해 보겠습니다.

services.AddAuthorization(options =>
{
    options.AddPolicy("AdultOnly", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));
});

위 예제에서는 사용자가 18세 이상일 경우에만 권한이 부여되는 정책을 정의합니다.

Requirement (요구 사항)

**요구 사항(Requirement)**은 정책의 조건을 정의하는 구성 요소입니다. 예를 들어, 사용자의 나이가 특정 값 이상이어야 하는지 확인하는 요구 사항을 만들 수 있습니다.

요구 사항 클래스 정의

IAuthorizationRequirement 인터페이스를 구현하여 요구 사항을 정의합니다.

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

이 클래스는 사용자가 충족해야 할 최소 연령을 요구 사항으로 정의합니다.

Handler (핸들러)

**핸들러(Handler)**는 요청이 특정 요구 사항을 만족하는지 확인합니다. 핸들러는 AuthorizationHandler<TRequirement>를 상속받아 구현할 수 있습니다.

핸들러 클래스 정의

아래 예제는 MinimumAgeRequirement 요구 사항을 처리하는 핸들러입니다.

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        MinimumAgeRequirement requirement)
    {
        var birthDateClaim = context.User.FindFirst(c => c.Type == "BirthDate");
        if (birthDateClaim == null)
        {
            return Task.CompletedTask;
        }

        var birthDate = Convert.ToDateTime(birthDateClaim.Value);
        var age = DateTime.Today.Year - birthDate.Year;

        if (birthDate > DateTime.Today.AddYears(-age)) age--;

        if (age >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

이 핸들러는 사용자의 생년월일(BirthDate) 클레임을 검사하여 최소 연령 요구 사항을 충족하는지 확인합니다. 조건을 만족하면 context.Succeed(requirement)를 호출하여 권한 부여를 승인합니다.

정책 적용

정의한 정책은 컨트롤러엔드포인트에 적용할 수 있습니다.

컨트롤러에 정책 적용

아래와 같이 [Authorize] 속성에 정책 이름을 지정하여 적용합니다.

[Authorize(Policy = "AdultOnly")]
public class AdultContentController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}
엔드포인트에 정책 적용

엔드포인트 라우팅을 사용할 경우 다음과 같이 정책을 적용할 수 있습니다.

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
    endpoints.MapGet("/adult", [Authorize(Policy = "AdultOnly")] () => "Adult Content");
});

결론

ASP.NET Core에서 정책 기반 권한 부여는 강력하고 유연한 권한 관리 방법입니다. 정책(Policy), 요구 사항(Requirement), 핸들러(Handler)를 통해 조건부 권한 부여를 구현할 수 있으며, 이를 사용하여 애플리케이션의 보안을 강화할 수 있습니다.

이와 같은 구조를 활용하면 복잡한 비즈니스 로직에 맞는 세분화된 권한 관리가 가능해집니다.

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://learn.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>

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

https://learn.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}");
}

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 사용하기

JSON Web Token 소개

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

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

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

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

JWT 인증을 위한 NuGet 패키지

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

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

토큰 생성 샘플 코드

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

***코드: ***

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) 프로젝트 생성

다음과 같은 템플릿으로 프로젝트를 생성합니다.

(2) NuGet 패키지 추가

JWT 인증을 위한 필수 패키지를 설치합니다.

  • JWT 토큰 처리 패키지
    • System.IdentityModel.Tokens.Jwt.dll
  • JWT Bearer 토큰 인증 패키지
    • Microsoft.AspNetCore.Authentication.JwtBearer.dll

(3) Startup.cs 파일 수정

Startup.cs 파일을 열어 JWT 관련 미들웨어를 추가합니다.

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

(4) AddJwtBearer() 메서드 세부 설정

AddJwtBearer() 메서드의 세부 옵션은 다음과 같습니다.
보안키는 반드시 긴 문자열로 설정하며, appsettings.json 또는 안전한 외부 저장소에 보관하는 것이 좋습니다.

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["SymmetricSecurityKey"])),
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        ClockSkew = TimeSpan.FromMinutes(5)
    };
});

(5) 회원 관련 모델 클래스 생성

SignBase.csSignViewModel.cs 모델 클래스를 생성합니다.

  • SignBase.cs
    • 데이터베이스 관련 클래스
    • 예: SignBases.sql, Signs.sql
  • SignViewModel.cs
    • 사용자 관련 뷰 모델
    • 예: UserViewModel.cs, LoginViewModel.cs

(6) 리포지토리 생성

ISignRepository.cs 인터페이스와 SignRepository.cs 클래스를 생성하여 데이터 접근 로직을 구현합니다. (세부 내용 생략)

(7) Web API 생성

SignServicesController.cs를 생성하여 API 엔드포인트를 추가합니다.

  • Login 메서드

    [HttpPost("Login")]
    public IActionResult Login([FromBody] SignViewModel model)
    {
        return Ok();
    }
    
  • LoginTest 메서드

    [HttpGet("LoginTest")]
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    public IActionResult LoginTest()
    {
        return Ok("인증된 사용자만 접근 가능");
    }
    

(8) 토큰 발행 공식 코드

다음은 로그인 성공 시 토큰을 발행하는 기본 코드입니다.

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

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

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

    // 토큰 생성
    var token = new JwtSecurityToken(
        claims: claims,
        signingCredentials: signingCredentials,
        expires: DateTime.Now.AddMinutes(5)
    );
    var tokenString = new JwtSecurityTokenHandler().WriteToken(token);

    return Ok(new { Token = tokenString });
}

(9) [Authorize] 특성 테스트

  • LoginTest 메서드 실행:
    • 인증 실패 시 접근 불가
    • 인증 성공 시 접근 가능

(10) 토큰 확인

발행된 토큰은 jwt.io에서 디코딩하여 확인할 수 있습니다.

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

(11) 보안 키 관리

보안 키 값은 appsettings.json에 저장하면 관리가 편리합니다. 예:

{
  "SymmetricSecurityKey": "긴_보안_키_문자열"
}

추가 사항

  • 토큰 발행 메서드 개선: 사용자 역할(Role)을 추가하여 클레임에 포함할 수 있습니다.
  • 토큰 만료 시간 연장: 특정 조건에서 만료 시간을 동적으로 설정할 수 있습니다.
  • 로그 아웃 처리: 토큰 무효화를 위한 메커니즘 추가 (예: 블랙리스트).

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

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

인증된 사용자에 대한 클레임 정보를 조회하면, 사용자에 대한 다양한 정보를 활용할 수 있습니다. 클레임은 사용자의 ID, 이름, 이메일 등과 같은 속성을 나타내며, 이를 통해 사용자 인증 및 권한 부여를 효과적으로 처리할 수 있습니다.

클레임 정보 조회 예제

아래 코드는 HTTP 요청 컨텍스트에서 클레임 정보를 조회하는 방법을 보여줍니다.

var identity = HttpContext.User.Identity as ClaimsIdentity;
if (identity != null)
{
    var userId = identity.FindFirst(ClaimTypes.NameIdentifier)?.Value; // 사용자 ID
    var userName = identity.FindFirst(ClaimTypes.Name)?.Value;         // 사용자 이름
    var userEmail = identity.FindFirst(ClaimTypes.Email)?.Value;       // 사용자 이메일

    // 클레임 정보 확인
    Console.WriteLine($"UserId: {userId}, UserName: {userName}, UserEmail: {userEmail}");
}

보충 설명

  1. HttpContext.User.Identity as ClaimsIdentity

    • 현재 인증된 사용자의 클레임을 가져오는 가장 일반적인 방법입니다.
    • ClaimsIdentity 개체는 사용자의 클레임 정보를 포함합니다.
  2. FindFirst 메서드

    • 특정 클레임 유형(예: ClaimTypes.Name, ClaimTypes.Email)을 검색하여 반환합니다.
    • 클레임이 없을 경우 null을 반환하므로 ?.Value를 사용해 안전하게 값을 조회합니다.
  3. 클레임 유형 예시:

    • ClaimTypes.NameIdentifier: 사용자의 고유 ID.
    • ClaimTypes.Name: 사용자의 이름.
    • ClaimTypes.Email: 사용자의 이메일 주소.
    • 필요에 따라 사용자 정의 클레임도 추가할 수 있습니다.

컨트롤러 예제

다음은 인증된 사용자의 이메일을 반환하는 Web API 엔드포인트입니다.

[HttpGet("LoginTest")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult LoginTest()
{
    // 인증된 사용자의 클레임 정보에서 이메일 반환
    var identity = HttpContext.User.Identity as ClaimsIdentity;
    var userEmail = identity?.FindFirst(ClaimTypes.Email)?.Value;

    if (userEmail == null)
    {
        return Unauthorized("사용자 이메일을 찾을 수 없습니다.");
    }

    return Ok($"인증된 사용자 이메일: {userEmail}");
}

확장 가능성

  • 추가 클레임 처리: 사용자 역할(Role), 권한, 추가 메타데이터를 클레임에 포함하고 처리할 수 있습니다.
  • 커스텀 클레임: 아래와 같이 사용자 지정 클레임을 추가해 관리할 수 있습니다.
    var claims = new[]
    {
        new Claim(ClaimTypes.Name, "UserName"),
        new Claim(ClaimTypes.Role, "Admin"), // 역할 추가
        new Claim("CustomClaimType", "CustomValue") // 커스텀 클레임
    };
    

클레임 활용 시 유의사항

  1. 보안: 민감한 데이터(예: 비밀번호, 보안 토큰 등)는 클레임에 저장하지 않는 것이 좋습니다.
  2. 토큰 크기 관리: 너무 많은 클레임을 추가하면 토큰 크기가 커질 수 있으므로 필요한 정보만 포함해야 합니다.
  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를 사용하여 인증 및 권한 관리를 보다 안전하게 수행할 수 있습니다.

ASP.NET Core MVC에서 AddHttpContextAccessor() 메서드

AddHttpContextAccessor() 메서드는 ASP.NET Core MVC 애플리케이션에서 IHttpContextAccessor 서비스를 등록하는 데 사용됩니다. 이 서비스를 통해 현재 HTTP 요청의 HttpContext에 접근할 수 있습니다.

사용 예제

Program.cs 파일 또는 Startup.cs 파일에서 AddHttpContextAccessor()를 사용하여 IHttpContextAccessor를 등록합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor(); // IHttpContextAccessor 서비스 등록

var app = builder.Build();

설명

  • AddHttpContextAccessor() 메서드: IHttpContextAccessor 인터페이스를 구현하는 서비스를 DI(의존성 주입) 컨테이너에 등록합니다.
  • IHttpContextAccessor: 현재 HTTP 요청의 HttpContext를 제공하는 인터페이스로, 컨트롤러나 서비스에서 현재 요청의 컨텍스트에 접근할 때 유용합니다.

실습 팁

  • 서비스 클래스에서 사용: 일반 서비스 클래스에서 현재 HttpContext에 접근하려면 IHttpContextAccessor를 생성자 주입으로 받아 사용합니다.
public class MyService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public MyService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void SomeMethod()
    {
        var context = _httpContextAccessor.HttpContext;
        // HttpContext를 사용하여 작업 수행
    }
}
  • 중간자(미들웨어)에서 사용: 미들웨어에서도 IHttpContextAccessor를 사용하여 요청 컨텍스트에 접근할 수 있습니다.

추가 정보

  • DI 컨테이너: ASP.NET Core의 의존성 주입 시스템에서 IHttpContextAccessor를 등록하여 필요할 때마다 주입받아 사용할 수 있습니다.
  • HttpContext: 현재 HTTP 요청의 모든 정보를 담고 있는 컨텍스트 객체로, 요청과 응답에 관한 다양한 정보를 제공합니다.

External Identity Providers

외부 아이덴터티 공급자 사용하기

기타

CSRF

CSRF(Cross-Site Request Forgery)는 악의적인 웹사이트가 사용자의 브라우저를 통해 다른 웹사이트로 원치 않는 요청을 보내도록 속이는 공격 기법입니다.

SameSite 쿠키 옵션

SameSite 쿠키 옵션은 웹 브라우저가 쿠키를 언제 전송할지를 제어하여 CSRF(Cross-Site Request Forgery) 공격을 방지하는 데 도움을 주는 보안 메커니즘입니다. 이 옵션은 쿠키의 보안 속성 중 하나로, 쿠키가 사이트 간 요청에 포함될 수 있는지 여부를 결정합니다.

SameSite 옵션 값

SameSite 옵션에는 세 가지 주요 값이 있습니다: Strict, Lax, None.

  1. Strict

    • 쿠키는 동일한 사이트의 요청에서만 전송됩니다.
    • 다른 사이트에서 유발된 모든 요청에 대해 쿠키가 전송되지 않습니다.
    • 가장 안전한 옵션으로, CSRF 공격을 효과적으로 방지합니다.
    • 예: 사용자가 다른 웹사이트에서 링크를 클릭해도 로그인 쿠키는 전송되지 않음.
    options.Cookie.SameSite = SameSiteMode.Strict;
    
  2. Lax

    • 쿠키는 "안전한" 요청(예: GET 메서드로 시작되는 톱 레벨 네비게이션)에서 전송됩니다.
    • POST 요청 같은 "비안전한" 메서드로 시작되는 요청에는 쿠키가 전송되지 않습니다.
    • 일반적인 사용 사례에서는 적절한 보안을 제공하며, 사용 편의성도 높습니다.
    • 예: 사용자가 링크를 클릭하거나 브라우저 주소창에 URL을 입력할 때는 쿠키가 전송되지만, 폼 제출 같은 POST 요청에는 전송되지 않음.
    options.Cookie.SameSite = SameSiteMode.Lax;
    
  3. None

    • 쿠키는 모든 종류의 요청에 대해 전송됩니다.
    • 보안을 위해 Secure 플래그를 설정해야 합니다.
    • 예: 쿠키가 모든 사이트 간 요청에 전송되며, 특히 제3자 서비스와의 통합에 사용됨.
    options.Cookie.SameSite = SameSiteMode.None;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    

설정 예제

ASP.NET Core에서 SameSite 옵션을 설정하는 예제는 다음과 같습니다.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.MinimumSameSitePolicy = SameSiteMode.Strict;
    });

    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                options.Cookie.SameSite = SameSiteMode.Strict;
                options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
            });

    services.AddControllersWithViews();
}

요약

  • Strict: 동일 사이트 요청에서만 쿠키 전송 (가장 안전).
  • Lax: 대부분의 동일 사이트 요청에서 쿠키 전송 (안전성과 편의성의 균형).
  • None: 모든 요청에서 쿠키 전송 (제3자 통합, Secure 플래그 필요).

SameSite 옵션을 적절히 설정함으로써 CSRF 공격을 방지하고 애플리케이션의 보안을 강화할 수 있습니다.

참고 강의

유튜브의 다음 강의가 ASP.NET Core 인증과 권한 부여 관련해서 전체 흐름을 이해할 수 있는데 좋은 자료입니다.

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