콘텐츠 대표 이미지 - ASP.NET Core 보안 완전정복: 인증과 권한 부여, 이제 헷갈리지 말자!

ASP.NET Core 보안 완전정복: 인증과 권한 부여, 이제 헷갈리지 말자!

클럽 문지기처럼 까다로운 웹 애플리케이션의 보안, 그 핵심인 '인증'과 '권한 부여'를 친구처럼 쉽고 재미있게 파헤쳐 줄게!

야, 너두 이제 보안 전문가 될 수 있어!

안녕! C#이랑 ASP.NET Core로 뭔가 멋진 걸 만들어보려고 코드를 짜고 있는 너, 정말 멋져.
근데 혹시 이런 생각해 본 적 없어? "내 앱, 아무나 막 들어와서 휘저으면 어떡하지?"

맞아. 우리가 열심히 만든 서비스에 '회원가입'과 '로그인' 기능이 있다면, 이제부터는 '보안'이라는 아주 중요한 관문을 넘어야 해. 이건 마치 클럽 입구에서 신분증 검사하는 '문지기'를 세우는 것과 같아. 아무나 들여보내지 않고, 또 들어온 사람이라도 VIP 룸은 못 들어가게 막는 거지.

웹 개발에서 이 문지기 역할을 하는 두 명의 핵심 플레이어가 바로 인증(Authentication)과 권한 부여(Authorization)야. 이름도 비슷해서 헷갈리기 딱 좋지? 걱정 마. 오늘 이 글 하나로 두 녀석의 개념부터 ASP.NET Core에서 실제로 어떻게 구현하는지까지, 아주 그냥 뿌리를 뽑아줄 테니까. 커피 한 잔 들고 편하게 따라와 봐!


1. 인증(AuthN) vs 권한 부여(AuthZ): 일단 개념부터 잡고 가자

코드를 보기 전에, 이 둘의 차이부터 확실히 알아야 해. 이걸 헷갈리면 나중에 코드가 산으로 가거든.

인증 (Authentication, 줄여서 AuthN): "누구세요?"

인증은 아주 간단해. **"네가 정말 네가 맞는지"** 확인하는 과정이야.
사용자가 아이디와 비밀번호를 입력하면, 우리 시스템은 "아, 이 사람은 '개발자A'가 맞구나!" 하고 신원을 확인해줘. 이게 바로 인증이야. 로그인 과정 그 자체라고 생각하면 돼.

  • 현실 예시: 클럽 입구에서 신분증 보여주는 것.
  • 웹 예시: 아이디/비밀번호, 지문 인식, 소셜 로그인으로 "나"임을 증명하는 것.

권한 부여 (Authorization, 줄여서 AuthZ): "들어오셨는데... 여긴 안돼요."

인증을 통과해서 일단 앱에 들어왔다고 해서 모든 걸 할 수 있는 건 아니야.
**"인증된 사용자가 특정 행동을 할 자격이 있는지"**를 확인하는 게 바로 권한 부여야. '개발자A'는 일반 사용자라서 글을 읽고 쓰는 건 되지만, 관리자 페이지에 접근하거나 다른 회원을 강퇴시킬 수는 없어야 하잖아? 이런 걸 체크하는 거지.

  • 현실 예시: 신분증 검사하고 클럽에 들어왔지만, VIP 룸은 VIP 팔찌가 있어야만 들어갈 수 있는 것.
  • 웹 예시: 일반 유저는 글쓰기만 가능, 관리자(Admin)는 모든 게시글을 삭제할 수 있는 권한을 갖는 것.

ASP.NET Core는 이 두 가지를 아주 멋지게 분리해서 처리해. 요청이 들어오면 먼저 **인증 미들웨어**가 "누구냐, 넌!" 하고 신원을 파악하고, 그 다음에 **권한 부여 미들웨어**가 "음, 넌 여기 들어올 자격이 있군/없군" 하고 판단하는 순서로 동작하지.

ASP.NET Core 보안: 인증(Authentication) vs 권한 부여(Authorization) 보안 처리 흐름도 STEP 1. 요청 STEP 2. 인증 STEP 3. 권한 부여 STEP 4. 결과 사용자 요청 Request 인증 (AuthN) Authentication "당신은 누구인가요?" 권한 부여 (AuthZ) Authorization "권한이 있나요?" 접근 허용 ✓ Success 인증 실패 401 Unauthorized 권한 없음 403 Forbidden 인증 성공 권한 확인 실패 실패

ASP.NET Core의 인증 및 권한 부여 처리 흐름도


2. ASP.NET Core 인증의 뼈대: Claims와 Principal 이해하기

자, 이제 슬슬 코드 맛을 볼 시간이야. ASP.NET Core에서 '인증된 사용자'는 그냥 '로그인 성공!'이라는 텍스트 쪼가리가 아니야. 아주 체계적인 객체로 관리되는데, 그 핵심이 바로 Claims(클레임)과 ClaimsPrincipal(클레임 주체)이야.

잠깐! 이걸 왜 알아야 해? 이 개념을 알아야 "로그인한 사용자의 이름이 뭐지?", "이 사용자는 관리자인가?", "이 사용자의 이메일 주소는?" 같은 정보들을 코드에서 자유자재로 꺼내 쓸 수 있어. 권한 부여 규칙을 만들 때도 필수적이야.

Claims: 사용자의 정보 조각들

클레임은 사용자에 대한 정보 하나하나를 의미해. 마치 신분증에 이름, 주민번호, 주소, 발급처가 적혀있는 것처럼 말이야.

  • `new Claim("name", "홍길동")` : 이름은 홍길동이다.
  • `new Claim("email", "gildong@example.com")` : 이메일은 gildong@example.com이다.
  • `new Claim(ClaimTypes.Role, "Admin")` : 역할(Role)은 관리자(Admin)다.

이렇게 (타입, 값) 쌍으로 이루어진 정보 조각 하나하나가 클레임이야. `ClaimTypes`는 `System.Security.Claims` 네임스페이스에 미리 정의된 상수들이라 이걸 쓰면 오타도 줄이고 코드도 깔끔해져.

ClaimsIdentity: 정보 조각을 모은 신분증

이 클레임들을 여러 개 모아서 하나의 '신원'을 구성한 게 바로 `ClaimsIdentity`야. 운전면허증이라는 신분증 하나에 이름, 주소, 면허 종류 같은 여러 정보(클레임)가 담겨있는 걸 생각하면 쉬워.


var claims = new List<Claim>
{
    new Claim(ClaimTypes.Name, "chulsoo"),
    new Claim("FullName", "김철수"),
    new Claim(ClaimTypes.Role, "User"),
};

var identity = new ClaimsIdentity(claims, "CookieAuth"); // "CookieAuth"는 인증 방식 이름

ClaimsPrincipal: 신분증을 소지한 '사람'

마지막으로 `ClaimsPrincipal`은 바로 '사용자' 그 자체를 나타내는 객체야. 그리고 이 사용자는 여러 개의 신분증(`ClaimsIdentity`)을 가질 수 있어. 예를 들어, 한 사람이 한국 운전면허증도 있고, 구글 계정으로도 인증할 수 있는 것처럼 말이지.


var userPrincipal = new ClaimsPrincipal(identity);

우리가 컨트롤러나 뷰에서 `HttpContext.User`로 접근하는 게 바로 이 `ClaimsPrincipal` 객체야. 그래서 우리는 `User.Identity.IsAuthenticated`로 로그인 여부를 확인하고, `User.FindFirst(ClaimTypes.Name)?.Value` 같은 코드로 사용자의 이름(chulsoo)을 꺼내올 수 있는 거지.

이 구조만 머릿속에 넣어두면, 앞으로 나올 인증 방식들이 훨씬 쉽게 이해될 거야. 결국 어떤 인증 방식을 쓰든, 최종 목표는 **"로그인한 사용자에 대한 클레임들을 만들어서 `HttpContext.User`에 넣어주는 것"**이거든.


3. 쿠키 인증: 전통의 강자, 아직 쓸만할까?

자, 첫 번째 인증 타자는 바로 **쿠키(Cookie) 인증**이야. 아마 웹 개발 좀 해봤다면 가장 친숙한 방식일 거야. 전통적인 서버 사이드 렌더링(SSR) 방식의 웹 앱(MVC, Razor Pages)에서 지금도 아주 널리 쓰이고 있어.

어떻게 돌아가는데?

  1. 로그인: 사용자가 아이디/비번을 보내면, 서버는 확인 후 "인증 성공!" 도장을 찍어줘.
  2. 쿠키 발급: 서버는 이 사용자의 정보(클레임들)를 암호화해서 '인증 쿠키'라는 과자 상자에 담아. 그리고 이걸 HTTP 응답에 실어서 브라우저에게 보내. "자, 이거 받아둬!"
  3. 쿠키 저장: 브라우저는 이 쿠키를 낼름 받아서 안전한 곳에 저장해.
  4. 요청마다 쿠키 전송: 이후 사용자가 우리 사이트의 다른 페이지를 요청할 때마다, 브라우저는 자기가 갖고 있던 그 인증 쿠키를 요청에 자동으로 붙여서 보내.
  5. 쿠키 확인: 서버는 요청이 올 때마다 쿠키를 까보고, "아, 김철수님이구나. 어서 와." 하고 사용자를 인식하는 거지.

장점: 구현이 비교적 간단하고, 브라우저가 알아서 쿠키를 관리해주니 편해. 상태가 서버에 저장되는(Stateful) 방식이라 특정 사용자를 강제로 로그아웃시키는 등의 제어가 쉬워.

단점: 쿠키는 동일한 도메인에만 전송되므로, 도메인이 다른 여러 서비스(예: 웹, 모바일 앱, 데스크톱 앱)에서 하나의 인증 시스템을 쓰기엔 부적합해. CSRF(Cross-Site Request Forgery) 공격에 취약할 수 있어서 `AntiForgeryToken` 같은 방어책이 필수야.

ASP.NET Core에서 쿠키 인증 설정하기

백문이 불여일견! `Program.cs` 파일에 어떻게 설정하는지 바로 코드로 보자. (.NET 6 이상 기준)

Program.cs

var builder = WebApplication.CreateBuilder(args);

// 1. 인증 서비스 등록
builder.Services.AddAuthentication("CookieAuth") // 기본 인증 스킴(Scheme) 이름을 "CookieAuth"로 지정
    .AddCookie("CookieAuth", options =>
    {
        options.Cookie.Name = "MyAwesomeCookie"; // 쿠키 이름 설정
        options.LoginPath = "/Account/Login";     // 인증이 필요할 때 리다이렉트될 로그인 페이지 경로
        options.AccessDeniedPath = "/Account/AccessDenied"; // 권한이 없을 때 보여줄 페이지 경로
        options.ExpireTimeSpan = TimeSpan.FromMinutes(30); // 쿠키 만료 시간
    });

// ... AddControllersWithViews(), AddRazorPages() 등등

var app = builder.Build();

// ...

app.UseRouting();

// 2. 인증 미들웨어 추가 (라우팅 다음에, 권한 부여 전에!)
app.UseAuthentication();
app.UseAuthorization();

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

app.Run();

딱 두 단계야.
1. `builder.Services.AddAuthentication().AddCookie()`로 쿠키 인증을 사용하겠다고 **서비스 컨테이너에 등록**하고,
2. `app.UseAuthentication()`으로 모든 요청에 대해 인증을 시도하는 **미들웨어를 파이프라인에 추가**하는 거지. 순서가 중요해! `UseRouting` 다음, `UseAuthorization` 전에 와야 해.

로그인 & 로그아웃 구현하기

이제 `AccountController` 같은 곳에서 실제 로그인/로그아웃 로직을 만들어보자.

Controllers/AccountController.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

public class AccountController : Controller
{
    [HttpGet]
    public IActionResult Login()
    {
        return View();
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        // 실제로는 DB에서 사용자 정보를 확인해야겠지?
        // 여기서는 간단하게 아이디/비번이 맞다고 가정.
        if (model.Username == "test" && model.Password == "1234")
        {
            // 1. 사용자에 대한 클레임 생성
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, model.Username),
                new Claim("FullName", "테스트 유저"),
                new Claim(ClaimTypes.Role, "User"),
            };

            // 2. ClaimsIdentity 생성
            var identity = new ClaimsIdentity(claims, "CookieAuth");

            // 3. ClaimsPrincipal 생성
            var principal = new ClaimsPrincipal(identity);

            // 4. HttpContext.SignInAsync를 사용하여 쿠키 생성 및 로그인 처리
            await HttpContext.SignInAsync("CookieAuth", principal);

            return RedirectToAction("Index", "Home");
        }

        ModelState.AddModelError(string.Empty, "Invalid login attempt.");
        return View(model);
    }

    public async Task<IActionResult> Logout()
    {
        // HttpContext.SignOutAsync를 사용하여 쿠키 삭제 및 로그아웃 처리
        await HttpContext.SignOutAsync("CookieAuth");
        return RedirectToAction("Index", "Home");
    }
}

핵심은 `HttpContext.SignInAsync()`와 `HttpContext.SignOutAsync()`야. `SignInAsync`를 호출하는 순간, ASP.NET Core가 알아서 클레임 정보들을 암호화해서 쿠키를 만들고 응답 헤더에 실어줘. 정말 편하지 않아?

이제 컨트롤러나 액션 메서드 위에 `[Authorize]` 특성만 붙여주면, 로그인하지 않은 사용자는 우리가 `LoginPath`에 설정한 `/Account/Login` 페이지로 자동으로 쫓겨나게 돼. 이게 쿠키 인증의 기본이야!


4. JWT 인증: API와 SPA 시대의 새로운 표준

쿠키가 전통적인 웹사이트의 문지기였다면, **JWT(Json Web Token, 발음은 '조트' 또는 '제이더블유티')**는 현대적인 웹 애플리케이션, 특히 SPA(Single Page Application)나 모바일 앱, 그리고 마이크로서비스 환경의 문지기라고 할 수 있어.

JWT, 넌 대체 뭐니?

JWT는 이름 그대로, 필요한 모든 정보를 자체적으로 가지고 있는 JSON 객체를 암호화해서 만든 토큰이야. 쿠키처럼 서버가 상태를 기억할 필요가 없어. 이걸 **Stateless(무상태)**라고 하는데, 이게 JWT의 가장 큰 특징이자 장점이야.

JWT는 `.`을 기준으로 세 부분으로 나뉘어.

ASP.NET Core JWT 토큰 구조 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ . SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c HEADER { "alg": "HS256", "typ": "JWT" } 알고리즘 및 토큰 타입 정의 PAYLOAD { "sub": "user123", "role": "Admin", "exp": 1672531199 } 사용자 정보 및 권한 데이터 SIGNATURE HMACSHA256( base64(header) + "." + base64(payload), secret_key ) 토큰 무결성 검증용 서명 🔒 ASP.NET Core에서 JWT는 인증(Authentication)과 권한부여(Authorization)의 핵심 메커니즘입니다

JWT(JSON Web Token)의 구조

  1. 헤더(Header): 토큰의 타입(JWT)과 어떤 알고리즘으로 서명했는지(예: HS256)를 담고 있어.
  2. 페이로드(Payload): 토큰에 담을 실제 정보, 즉 클레임들이 들어가는 부분이야. 사용자의 아이디, 이름, 역할, 토큰 만료 시간(`exp`) 등을 여기에 넣어. **주의!** 페이로드는 그냥 Base64로 인코딩된 거라 누구나 디코딩해서 볼 수 있어. 그러니 비밀번호 같은 민감한 정보는 절대 넣으면 안 돼!
  3. 서명(Signature): 이게 JWT의 핵심이야. (헤더 인코딩 값 + `.` + 페이로드 인코딩 값)을 합친 뒤, 서버만 알고 있는 **비밀 키(Secret Key)**로 암호화해서 만들어. 이 서명 덕분에 누군가 중간에 페이로드 내용을 조작해도 서버가 바로 알아챌 수 있어. "어? 서명이 다르네? 이거 위조된 토큰이군!" 하고 말이지.

어떻게 돌아가는데? (API 서버 관점)

  1. 로그인 & 토큰 발급: 사용자가 아이디/비번으로 로그인하면, 서버는 확인 후 JWT를 생성해서 사용자에게 던져줘. "자, 이게 너의 출입증이야. 유효기간은 1시간!"
  2. 토큰 저장: 클라이언트(웹 브라우저의 경우 Local Storage나 Session Storage, 모바일 앱의 경우 Secure Storage)는 이 토큰을 저장해.
  3. API 요청: 이후 보호된 API를 호출할 때마다, 클라이언트는 HTTP 요청 헤더의 `Authorization` 필드에 토큰을 실어 보내. 보통 이런 형식이야: `Authorization: Bearer eyJhbGciOi...`
  4. 토큰 검증: API 서버는 요청 헤더에 담겨온 토큰을 받아.
    • 먼저 서명을 자기가 가진 비밀 키로 검증해서 위변조 여부를 확인해.
    • 그 다음 페이로드의 만료 시간(`exp`)을 보고 토큰이 유효한지 확인해.
    • 모든 검증을 통과하면, 페이로드에 담긴 클레임들을 바탕으로 사용자를 인식하고 요청을 처리해줘.

서버는 세션 저장소 같은 걸 뒤져볼 필요가 없어. 그냥 토큰만 보고 모든 걸 판단하지. 그래서 서버를 여러 대로 늘리는 스케일 아웃 환경에서도 아주 잘 동작해. A 서버에서 발급한 토큰을 B 서버에서도 문제없이 검증할 수 있으니까!

ASP.NET Core에서 JWT 인증 설정하기

JWT 인증을 쓰려면 `Microsoft.AspNetCore.Authentication.JwtBearer` NuGet 패키지를 먼저 설치해야 해.

Program.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;

// 1. 인증 서비스 등록
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        // 토큰 발급자(Issuer) 검증
        ValidateIssuer = true,
        ValidIssuer = configuration["Jwt:Issuer"],

        // 토큰 수신자(Audience) 검증
        ValidateAudience = true,
        ValidAudience = configuration["Jwt:Audience"],

        // 토큰 서명 키 검증
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"])),

        // 토큰 만료 시간 검증
        ValidateLifetime = true,

        // 클럭 스큐(Clock Skew) 설정 (서버와 클라이언트 시간 차이 허용)
        ClockSkew = TimeSpan.Zero
    };
});

// ...

var app = builder.Build();

// ...

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

// ...

쿠키 때랑 비슷하지만 설정이 좀 더 많지? `TokenValidationParameters`에서 어떤 것들을 검증할지 꼼꼼하게 설정해줘야 해. `Issuer`(누가 발급했나), `Audience`(누구를 위한 토큰인가), `IssuerSigningKey`(서명 키) 같은 값들은 보안을 위해 `appsettings.json` 같은 설정 파일에 빼두는 게 좋아.

appsettings.json

{
  "Jwt": {
    "Key": "ThisIsMySuperSecretKeyForJwtTokenGeneration12345", // 아주 길고 복잡한 문자열을 써야 해!
    "Issuer": "https://my-api.com",
    "Audience": "https://my-client.com"
  },
  // ...
}

매우 중요: 저 `Jwt:Key` 값은 절대 외부에 노출되면 안 돼! 저게 털리면 누구나 유효한 토큰을 만들어낼 수 있거든. 실제 프로덕션 환경에서는 Azure Key Vault나 AWS Secrets Manager 같은 보안 저장소에 보관해야 해.

토큰 생성 및 발급하기

로그인에 성공한 사용자에게 JWT를 만들어주는 코드를 보자.

Controllers/AuthController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase
{
    private readonly IConfiguration _config;

    public AuthController(IConfiguration config)
    {
        _config = config;
    }

    [HttpPost("login")]
    public IActionResult Login([FromBody] LoginModel login)
    {
        // 실제 DB 검증 로직...
        if (login.Username == "test" && login.Password == "1234")
        {
            var token = GenerateJwtToken(login.Username);
            return Ok(new { token });
        }
        return Unauthorized();
    }

    private string GenerateJwtToken(string username)
    {
        // 1. 클레임 생성
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, username), // sub는 고유 식별자를 의미
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // Jti는 토큰의 고유 ID
            new Claim(ClaimTypes.Role, "User")
        };

        // 2. 서명 키 생성
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        // 3. 토큰 만료 시간 설정
        var expires = DateTime.UtcNow.AddMinutes(30);

        // 4. JwtSecurityToken 객체 생성
        var token = new JwtSecurityToken(
            issuer: _config["Jwt:Issuer"],
            audience: _config["Jwt:Audience"],
            claims: claims,
            expires: expires,
            signingCredentials: creds
        );

        // 5. 토큰을 문자열로 직렬화
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

쿠키 인증 때처럼 클레임을 만들고, `appsettings.json`에서 읽어온 비밀 키로 서명(`SigningCredentials`)을 만들고, 만료 시간을 정해서 `JwtSecurityToken` 객체를 생성해. 마지막으로 `JwtSecurityTokenHandler`를 사용해서 이 객체를 우리가 아는 `eyJ...` 형태의 문자열로 바꿔주면 끝!

이제 클라이언트는 이 토큰을 받아서, `[Authorize]`가 붙은 API를 호출할 때마다 `Authorization: Bearer {토큰}` 헤더에 담아 보내주기만 하면 돼.


5. 권한 부여의 디테일: 문지기에게 규칙 알려주기

자, 이제 인증은 끝났어. 사용자가 누구인지는 알게 됐지. 이제부터는 "그래서 이 사용자가 뭘 할 수 있는데?"를 결정하는 **권한 부여(Authorization)**의 세계로 들어가 보자. ASP.NET Core는 정말 강력하고 유연한 권한 부여 시스템을 제공해.

가장 쉬운 방법: `[Authorize]` 특성

가장 기본은 그냥 `[Authorize]`를 붙이는 거야. 컨트롤러 클래스나 개별 액션 메서드 위에 이걸 붙이면, "인증된 사용자만 접근 가능"이라는 규칙이 생겨. 비로그인 사용자가 접근하면 401 Unauthorized 응답(API의 경우)이나 로그인 페이지로 리다이렉트(쿠키 인증의 경우)시켜 버리지.


[Authorize] // 이 컨트롤러의 모든 액션은 로그인이 필요함
public class SecretController : Controller
{
    public IActionResult MySecrets()
    {
        return View();
    }
}

역할 기반 권한 부여 (Role-Based Authorization)

하지만 "로그인만 하면 다 돼!"는 너무 단순하잖아. 보통은 역할에 따라 권한을 나누지. "관리자만 이 기능을 쓸 수 있어!" 같은 경우 말이야. 이건 `[Authorize]`에 Roles 파라미터를 추가하면 돼.


public class AdminController : Controller
{
    [Authorize(Roles = "Admin")] // "Admin" 역할을 가진 사용자만 접근 가능
    public IActionResult Dashboard()
    {
        // ...
    }

    [Authorize(Roles = "Admin,SuperUser")] // "Admin" 또는 "SuperUser" 역할을 가진 사용자
    public IActionResult ManageUsers()
    {
        // ...
    }
}

이게 동작하려면, 로그인할 때 사용자에게 해당 역할 클레임을 넣어줘야 해. 기억나? `new Claim(ClaimTypes.Role, "Admin")` 이렇게 말이야.

역할 기반의 한계: 역할 기반은 간단해서 좋지만, 규칙이 복잡해지면 금방 지저분해져. "20살 이상이고, VIP 멤버십이 있으면서, 마케팅 부서 소속인 사람만" 같은 규칙을 역할로만 표현하려면 `VIP_OVER20_MARKETING` 같은 괴상한 역할을 계속 만들어야 할까? 아니지! 그래서 등장한 게 바로 정책 기반 권한 부여야.

정책 기반 권한 부여 (Policy-Based Authorization): 진짜배기 고수의 기술

정책(Policy)은 "하나 이상의 요구사항(Requirement)들의 묶음"이야. 역할 기반보다 훨씬 유연하고, 재사용 가능하고, 코드를 깔끔하게 유지할 수 있어. 이게 ASP.NET Core 권한 부여의 꽃이야.

정책 기반 권한 부여는 3단계로 이루어져.

1단계: 정책 등록 (`Program.cs`)

`AddAuthorization`을 사용해서 정책을 정의하고 등록해.


builder.Services.AddAuthorization(options =>
{
    // "AdminOnly" 라는 이름의 정책 정의
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));

    // "Over20" 이라는 이름의 정책 정의
    // "DateOfBirth" 라는 클레임이 반드시 있어야 하고, 그 값을 계산해서 20살 이상인지 확인
    options.AddPolicy("Over20", policy =>
        policy.RequireAssertion(context =>
        {
            var dobClaim = context.User.FindFirst(c => c.Type == "DateOfBirth");
            if (dobClaim == null) return false;

            var dob = DateTime.Parse(dobClaim.Value);
            int age = DateTime.Today.Year - dob.Year;
            if (dob.Date > DateTime.Today.AddYears(-age)) age--;
            
            return age >= 20;
        }));

    // 여러 요구사항을 조합한 정책
    options.AddPolicy("MarketingVip", policy =>
    {
        policy.RequireRole("VipUser");
        policy.RequireClaim("Department", "Marketing");
    });
});

봐봐. `RequireRole`이나 `RequireClaim` 같은 기본 요구사항도 있고, `RequireAssertion`을 쓰면 람다식으로 복잡한 커스텀 로직도 넣을 수 있어. 훨씬 강력하지?

2단계: 정책 적용 (`[Authorize(Policy = "정책이름")]`)

이제 컨트롤러에서는 그냥 정책 이름만 불러다 쓰면 돼.


[Authorize(Policy = "AdminOnly")]
public IActionResult AdminDashboard() { ... }

[Authorize(Policy = "Over20")]
public IActionResult AdultContent() { ... }

[Authorize(Policy = "MarketingVip")]
public IActionResult MarketingCampaign() { ... }

어때? 컨트롤러 코드가 훨씬 깔끔해졌지? 권한 규칙이 바뀌어도 컨트롤러는 건드릴 필요 없이 `Program.cs`의 정책 정의 부분만 수정하면 돼. 이게 바로 관심사의 분리(Separation of Concerns) 원칙이야.

3단계 (고급): 커스텀 요구사항과 핸들러 만들기

더 복잡하고 재사용 가능한 규칙을 만들고 싶다면, `IAuthorizationRequirement`와 `AuthorizationHandler`를 직접 구현할 수 있어. 예를 들어 "최소 가입 기간" 같은 요구사항을 만들어보자.

요구사항(Requirement) 정의:


// 이 요구사항은 최소 몇 개월이 지났는지를 나타냄
public class MinimumMembershipRequirement : IAuthorizationRequirement
{
    public int MinimumMonths { get; }

    public MinimumMembershipRequirement(int minimumMonths)
    {
        MinimumMonths = minimumMonths;
    }
}

핸들러(Handler) 구현:


public class MinimumMembershipHandler : AuthorizationHandler<MinimumMembershipRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumMembershipRequirement requirement)
    {
        var joinDateClaim = context.User.FindFirst(c => c.Type == "JoinDate");
        if (joinDateClaim == null)
        {
            return Task.CompletedTask; // 클레임 없으면 처리 안함
        }

        var joinDate = DateTime.Parse(joinDateClaim.Value);
        var monthsOfMembership = (DateTime.UtcNow.Year - joinDate.Year) * 12 + DateTime.UtcNow.Month - joinDate.Month;

        if (monthsOfMembership >= requirement.MinimumMonths)
        {
            context.Succeed(requirement); // 요구사항 충족! 성공 처리
        }

        return Task.CompletedTask;
    }
}

서비스 및 정책 등록:


// 핸들러를 서비스에 등록
builder.Services.AddSingleton<IAuthorizationHandler, MinimumMembershipHandler>();

// 정책에 커스텀 요구사항 추가
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("LoyalCustomer", policy => 
        policy.AddRequirements(new MinimumMembershipRequirement(6))); // 6개월 이상 가입자
});

이제 `[Authorize(Policy = "LoyalCustomer")]`를 사용하면, 가입한 지 6개월 이상 된 사용자만 접근할 수 있는 기능을 만들 수 있어. 이런 식으로 비즈니스 로직과 관련된 복잡한 권한 규칙들을 별도의 클래스로 깔끔하게 분리하고 관리할 수 있게 되는 거야. 이게 정책 기반 권한 부여의 진정한 힘이지!


6. 소셜 로그인 연동: 구글, 페북 계정으로 1초 만에 로그인!

요즘 세상에 누가 일일이 회원가입해? 많은 사용자들이 구글, 페이스북, 카카오 같은 기존 계정을 사용해서 로그인하는 걸 선호하지. 이걸 우리 앱에 붙이는 건 생각보다 훨씬 쉬워. ASP.NET Core가 외부 인증 공급자(External Authentication Provider) 연동을 정말 잘 지원해주거든.

이 기술의 배경에는 OAuth 2.0과 OpenID Connect (OIDC)라는 표준 프로토콜이 있어.

  • OAuth 2.0: "권한 위임"을 위한 프로토콜이야. "내 구글 계정아, 저 '재능넷'이라는 앱이 내 프로필 정보를 읽어갈 수 있도록 허락해줘" 같은 시나리오에 쓰여.
  • OIDC: OAuth 2.0 위에 만들어진 "인증"을 위한 얇은 레이어야. OAuth 2.0이 권한만 위임한다면, OIDC는 사용자 정보(ID 토큰)까지 표준화된 방식으로 가져와서 "이 사용자가 누구인지"를 알려줘. 우리가 흔히 말하는 '소셜 로그인'은 대부분 OIDC를 사용해.

복잡해 보이지만 걱정 마. 우리는 그냥 NuGet 패키지 몇 개 설치하고 코드 몇 줄만 추가하면 돼!

구글 로그인 연동하기

예를 들어 구글 로그인을 연동해보자.

1. NuGet 패키지 설치

`Microsoft.AspNetCore.Authentication.Google` 패키지를 설치해.

2. 구글 클라우드 플랫폼에서 클라이언트 ID/Secret 발급

이건 코드 밖에서 해야 할 일이야. Google Cloud Platform에 가서 새 프로젝트를 만들고, 'API 및 서비스' > '사용자 인증 정보' 메뉴에서 'OAuth 클라이언트 ID'를 만들어야 해. 이때 우리 웹 앱의 주소(예: `https://localhost:7001/signin-google`)를 '승인된 리디렉션 URI'로 등록해야 해. 그러면 '클라이언트 ID'와 '클라이언트 보안 비밀'을 줄 거야. 이 값들을 `appsettings.json`이나 User Secrets에 잘 보관해둬.

3. `Program.cs`에 구글 인증 추가

기존 `AddAuthentication` 코드에 `AddGoogle`을 체인으로 연결해주면 돼.


builder.Services.AddAuthentication("CookieAuth") // 소셜 로그인 정보도 결국 쿠키에 저장할 거니까
    .AddCookie("CookieAuth", options => { /* ... */ })
    .AddGoogle(options =>
    {
        options.ClientId = configuration["Authentication:Google:ClientId"];
        options.ClientSecret = configuration["Authentication:Google:ClientSecret"];
    });

이게 다야! 정말이지?

4. 로그인/로그아웃 로직 수정

이제 로그인 페이지에 "구글로 로그인" 버튼을 만들고, 이 버튼이 특정 컨트롤러 액션을 호출하도록 해야 해.

Controllers/AccountController.cs

// "구글로 로그인" 버튼을 눌렀을 때 호출될 액션
[HttpPost]
public IActionResult ExternalLogin(string provider, string returnUrl = null)
{
    // 인증 속성을 설정하고 외부 공급자(구글)에게 인증을 요청
    var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl });
    var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
    return Challenge(properties, provider); // "provider"에는 "Google"이 들어옴
}

// 구글에서 인증 후 우리 앱으로 다시 돌아왔을 때 호출될 콜백 액션
[HttpGet]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
    // ... 에러 처리 ...

    // 외부 로그인 정보(구글이 제공한 사용자 정보)를 가져옴
    var info = await HttpContext.AuthenticateAsync("Identity.External");
    if (info == null)
    {
        return RedirectToAction(nameof(Login));
    }

    // info.Principal을 사용해서 우리 시스템에 로그인 처리 (SignInAsync)
    // 이때, 우리 DB에 해당 사용자가 있는지 확인하고, 없으면 새로 가입시키는 로직이 필요
    // 예를 들어, 구글이 준 이메일 클레임을 사용해서 우리 DB의 사용자를 찾거나 생성
    var email = info.Principal.FindFirstValue(ClaimTypes.Email);
    // ... (DB 조회 및 가입/로그인 처리 로직) ...

    // 우리 시스템의 쿠키로 로그인
    // await HttpContext.SignInAsync("CookieAuth", 우리시스템의_유저_Principal);
    
    return RedirectToLocal(returnUrl);
}

`Challenge`를 호출하면 ASP.NET Core가 알아서 사용자를 구글 로그인 페이지로 보내. 사용자가 구글에서 로그인하고 동의하면, 구글은 우리 앱의 콜백 URL(`signin-google`)로 사용자를 돌려보내. 그러면 `ExternalLoginCallback` 액션이 실행되고, `HttpContext.AuthenticateAsync`로 구글이 제공한 사용자 정보(`ClaimsPrincipal`)를 얻을 수 있어. 이 정보(이름, 이메일 등)를 바탕으로 우리 시스템에 회원가입시키거나 로그인 처리를 하고, 최종적으로 우리 앱의 인증 쿠키(`SignInAsync`)를 발급해주면 끝!

이런 편리함 덕분에 재능넷 같은 많은 플랫폼들이 소셜 로그인을 적극적으로 도입해서 사용자 경험을 높이고 있지. 우리도 충분히 할 수 있어!


7. 보안 강화 꿀팁: 이것만은 꼭 챙기자!

지금까지 배운 것만으로도 꽤 훌륭한 인증/권한 부여 시스템을 만들 수 있어. 하지만 진짜 프로 개발자가 되려면 몇 가지 보안 강화 포인트를 더 챙겨야 해.

1. HTTPS는 기본 중의 기본

이건 선택이 아니라 필수야. HTTP는 모든 데이터가 평문으로 전송돼. 로그인 정보, 쿠키, JWT 토큰이 중간에 가로채기(Sniffing) 당하면 그냥 다 털리는 거야. 무조건 HTTPS를 사용해야 해. ASP.NET Core는 개발 환경에서 기본적으로 HTTPS를 사용하도록 설정되어 있고, 프로덕션 환경에서는 `app.UseHttpsRedirection()` 미들웨어를 사용해서 모든 HTTP 요청을 HTTPS로 강제 리디렉션해야 해.

2. 비밀 키 관리 (Secret Management)

`appsettings.json`에 JWT 비밀 키나 구글 클라이언트 시크릿을 넣어두는 건 개발할 때나 쓰는 방법이야. 이 파일이 Git 저장소 같은 곳에 올라가면 큰일 나!

  • 개발 환경: User Secrets 도구를 사용해. 이건 프로젝트 폴더 바깥, 네 PC의 안전한 곳에 설정 값을 저장하는 방법이야.
  • 프로덕션 환경: Azure Key Vault, AWS Secrets Manager, HashiCorp Vault 같은 전문 보안 저장소 서비스를 사용하거나, 최소한 **환경 변수**를 사용해서 코드로 관리되지 않도록 해야 해.

3. CORS (Cross-Origin Resource Sharing)

만약 네가 만든 API 서버(`api.myapp.com`)를 다른 도메인에서 실행되는 웹 프론트엔드(`www.myapp.com`)에서 호출하려면, 브라우저의 동일 출처 정책(Same-Origin Policy) 때문에 막힐 거야. 이걸 해결하려면 서버에서 "응, 저 도메인에서 오는 요청은 받아도 괜찮아"라고 허락해줘야 하는데, 이게 바로 CORS 설정이야.


// Program.cs
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy =>
                      {
                          policy.WithOrigins("https://www.myapp.com", "https://staging.myapp.com")
                                .WithHeaders("Authorization", "Content-Type")
                                .WithMethods("GET", "POST", "PUT");
                      });
});

// ...

var app = builder.Build();

// ...

app.UseCors(MyAllowSpecificOrigins); // 라우팅 전에 호출

app.UseRouting();

// ...

`WithOrigins`에 허용할 프론트엔드 도메인을 명시해주면 돼. `*`로 모든 도메인을 허용하는 건 매우 위험하니, 꼭 필요한 도메인만 지정해주는 게 좋아.

4. CSRF 방어 (쿠키 인증 시 필수)

CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)는 쿠키 인증의 고질적인 약점이야. 공격자가 만든 악성 사이트(예: `evil.com`)에 사용자가 방문했을 때, 그 사이트가 내 은행 사이트(`mybank.com`)에 "100만원 송금" 요청을 몰래 보내는 공격이야. 브라우저는 `mybank.com`으로 요청을 보낼 때 자기가 가진 `mybank.com`의 인증 쿠키를 자동으로 붙여서 보내기 때문에, 은행 서버는 정상적인 사용자의 요청으로 착각하고 송금을 해버릴 수 있어.

ASP.NET Core는 이걸 막기 위해 **안티포저리 토큰(AntiForgeryToken)** 메커니즘을 제공해.

  • `
    ` 태그 안에 `@Html.AntiForgeryToken()` 헬퍼를 추가하면, 숨겨진 필드로 랜덤한 토큰 값이 생성돼.
  • 서버는 이 토큰을 쿠키로도 보내.
  • 폼이 전송될 때, 서버는 폼 데이터로 넘어온 토큰과 쿠키로 넘어온 토큰이 일치하는지 확인해.
  • 두 값이 일치해야만 요청을 처리해. 공격자는 쿠키는 보낼 수 있어도 폼 안에 숨겨진 토큰 값은 알 수 없기 때문에 공격이 실패하게 돼.

컨트롤러 액션에는 `[ValidateAntiForgeryToken]` 특성을 붙여주기만 하면 이 검증이 자동으로 이루어져. Razor Pages나 MVC를 쓴다면 이건 거의 필수야!

5. 토큰 만료와 리프레시 토큰 (JWT 사용 시)

JWT의 단점 중 하나는 한 번 발급되면 만료 전까지는 무효화하기 어렵다는 거야. 그래서 보통 JWT **액세스 토큰(Access Token)**의 유효기간은 15분~1시간 정도로 짧게 설정해.

"그럼 15분마다 다시 로그인해야 해?" 아니지! 이럴 때 쓰는 게 **리프레시 토큰(Refresh Token)**이야.

  • 액세스 토큰: 유효기간이 짧고, 실제 API 요청에 사용돼. 탈취되어도 피해를 최소화할 수 있어.
  • 리프레시 토큰: 유효기간이 길고(예: 7일), 새로운 액세스 토큰을 발급받는 용도로만 사용돼. DB 같은 안전한 곳에 저장하고 관리해야 해.

흐름:
1. 최초 로그인 시, 서버는 액세스 토큰과 리프레시 토큰을 둘 다 발급해줘.
2. 클라이언트는 두 토큰을 모두 저장해.
3. API 요청 시에는 액세스 토큰만 사용해.
4. API 서버가 "토큰 만료됨(401 Unauthorized)" 응답을 보내면,
5. 클라이언트는 가지고 있던 리프레시 토큰을 특별한 엔드포인트(예: `/auth/refresh`)로 보내.
6. 서버는 리프레시 토큰이 유효한지 DB에서 확인하고, 맞다면 새로운 액세스 토큰을 발급해줘.
7. 클라이언트는 이 새 액세스 토큰으로 다시 API 요청을 시도해.

이러면 사용자는 로그인이 끊기는 경험 없이 서비스를 계속 이용할 수 있고, 보안성도 높일 수 있어. 리프레시 토큰을 구현하는 건 조금 더 복잡하지만, 상용 서비스라면 반드시 고려해야 할 중요한 패턴이야.


마치며: 이제 문단속은 자신 있지?

와, 정말 긴 여정이었어! 처음엔 막막했던 인증과 권한 부여가 이제는 좀 만만하게 느껴지지 않아?

오늘 우리는 ASP.NET Core 보안의 두 기둥인 **인증(AuthN)**과 **권한 부여(AuthZ)**의 개념을 확실히 잡았어. 전통적인 웹 앱에 강한 **쿠키 인증**과, 현대적인 API와 SPA에 최적화된 **JWT 인증**의 작동 방식과 구현법을 모두 훑어봤지.

그리고 단순한 역할 기반을 넘어, 훨씬 유연하고 강력한 **정책 기반 권한 부여**의 세계까지 맛봤어. 이걸 잘 활용하면 아무리 복잡한 비즈니스 규칙이라도 깔끔하게 코드로 녹여낼 수 있을 거야.

보안은 한 번 설정하고 끝나는 게 아니야. 우리가 만든 앱이 성장하고 변화하는 만큼, 보안 정책도 계속해서 다듬고 발전시켜야 하는 살아있는 유기체와 같아. 오늘 배운 지식들이 네가 더 안전하고 신뢰할 수 있는 애플리케이션을 만드는 데 튼튼한 초석이 되길 바라.

이제 너는 클럽 문 앞에서 어리버리하게 서 있는 사람이 아니라, 누가 들어오고 나가는지, 누가 VIP 룸에 들어갈 자격이 있는지 정확히 통제할 수 있는 베테랑 문지기가 된 거야. 자, 이제 자신감을 갖고 너의 멋진 서비스를 만들러 가보자고!

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.