ASP.NET Core 4.2 · 4부. 인증·인가와 미들웨어

인증과 인가

둘이 어떻게 다른지 구분합니다.

예상 학습 시간 20분 실행 단추가 있는 예제는 고쳐서 실행해 볼 수 있습니다 난이도 중급
개념 설명

누구인가와 무엇을 할 수 있는가

2.5에서 401과 403을 자주 섞는다고 했습니다. 그 둘이 이 단원의 두 축입니다.

  • 인증(Authentication)누구인지 알아냅니다. 쿠키나 토큰을 읽어 신분을 세웁니다. 실패하면 401 입니다.
  • 인가(Authorization) — 그 사람이 이것을 해도 되는지 봅니다. 실패하면 403 입니다.

순서가 정해져 있습니다. 누구인지 모르면 무엇을 할 수 있는지도 알 수 없습니다. 그래서 인증이 먼저입니다.

이 사이트도 같은 방식으로 되어 있습니다. 관리자 화면은 로그인하지 않으면 401, 로그인했더라도 관리자가 아니면 403 입니다.

최소 예제

세 가지 문을 만들어 두고

누구나 볼 수 있는 곳, 로그인해야 하는 곳, 역할이 있어야 하는 곳을 두었습니다.

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

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();   // 먼저 — 누구인지 알아냅니다
app.UseAuthorization();    // 그 다음 — 해도 되는지 봅니다

app.MapGet("/public", () => "누구나 봅니다.");

app.MapGet("/mine", (ClaimsPrincipal me) => $"{me.Identity!.Name} 님")
   .RequireAuthorization();

app.MapGet("/admin", () => "관리자만")
   .RequireAuthorization(p => p.RequireRole("admin"));
받은 상태 코드
/public /mine /admin 로그인하지 않음 200 401 401 홍길동 (user) 200 200 403 김철수 (admin) 200 200 200

이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다.

가운데 줄이 이 단원의 전부입니다. 홍길동은 /mine 은 되는데 /admin 은 403 입니다. 누구인지는 알았으므로 401이 아닙니다. 다시 로그인하라고 해 봐야 소용없습니다.

컨트롤러에서는 어트리뷰트로 적습니다. 클래스에 붙이면 그 안의 모든 메서드에 적용됩니다.

[Authorize]                        // 로그인만 하면 됩니다
[Authorize(Roles = "admin")]        // 역할이 있어야 합니다
[AllowAnonymous]                  // 위에 붙은 것을 이 메서드만 풉니다
상세 사용법

인가가 보는 것은 클레임입니다

로그인하면 그 사람에 대해 아는 것들이 쿠키에 담깁니다. 하나하나가 클레임입니다. 인가는 이 목록만 봅니다. 그때마다 데이터베이스를 다시 읽지 않습니다.

순수한 객체라 서버 없이도 만들어 볼 수 있습니다.

Program.cs
using System.Security.Claims;

var 신분 = new ClaimsIdentity([
    new Claim(ClaimTypes.Name, "홍길동"),
    new Claim(ClaimTypes.Role, "user"),
    new Claim(ClaimTypes.Role, "editor"),
    new Claim("나이", "20")
], "쿠키");

var 사람 = new ClaimsPrincipal(신분);

Console.WriteLine("확인됨 : " + 사람.Identity!.IsAuthenticated);
Console.WriteLine("이름   : " + 사람.Identity.Name);
Console.WriteLine();

Console.WriteLine("담긴 것");
foreach (var c in 사람.Claims) Console.WriteLine("  " + c.Type + " = " + c.Value);

Console.WriteLine();
Console.WriteLine("user 인가?   " + 사람.IsInRole("user"));
Console.WriteLine("admin 인가?  " + 사람.IsInRole("admin"));
Console.WriteLine("나이         " + 사람.FindFirst("나이")?.Value);

var 빈사람 = new ClaimsPrincipal(new ClaimsIdentity());
Console.WriteLine();
Console.WriteLine("로그인하지 않은 사람의 확인됨 : " + 빈사람.Identity!.IsAuthenticated);
출력
확인됨 : True 이름 : 홍길동 담긴 것 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name = 홍길동 http://schemas.microsoft.com/ws/2008/06/identity/claims/role = user http://schemas.microsoft.com/ws/2008/06/identity/claims/role = editor 나이 = 20 user 인가? True admin 인가? False 나이 20 로그인하지 않은 사람의 확인됨 : False

코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.

세 가지가 보입니다.

  • ClaimTypes.Name긴 주소로 풀립니다. 오래전에 정해진 이름이라 그렇습니다. 우리가 만든 나이 처럼 짧은 이름을 그대로 사용해도 됩니다.
  • 같은 종류를 여럿 담을 수 있습니다. 역할이 둘이면 두 줄입니다.
  • IsAuthenticated인증 방식 이름을 넘겼는지로 정해집니다. 위에서 "쿠키" 를 넘겼기에 true 이고, 넘기지 않은 아래쪽은 false 입니다.

역할로 모자랄 때는 정책으로

"관리자여야 한다" 는 역할로 되지만 "열아홉 살이 넘어야 한다" 는 되지 않습니다. 그때 정책을 만듭니다.

Program.cs
builder.Services.AddAuthorization(o => {
    o.AddPolicy("어른", p => p.RequireAssertion(c =>
        int.TryParse(c.User.FindFirst("나이")?.Value, out var n) && n >= 19));
});

app.MapGet("/adult", () => "어른만").RequireAuthorization("어른");
받은 상태 코드
로그인하지 않음 401 홍길동 (user, 20세) 200 김철수 (admin, 15세) 403 ← 관리자여도 나이가 모자랍니다

마지막 줄이 정책의 값입니다. 역할과 무관하게 판정합니다. 조건이 여럿이면 RequireClaim·RequireRole 을 이어 붙이거나, 더 복잡하면 요구 사항(Requirement)과 처리기를 따로 만듭니다.

상세 사용법

순서를 뒤바꾸면 로그인해도 401 입니다

1.3에서 예고한 자리입니다. 실제로 뒤바꿔 보았습니다.

Program.cs
app.UseAuthorization();    // 먼저 두면
app.UseAuthentication();
로그인한 뒤에 받은 것
/public 200 /mine 401 ← 로그인했는데도 /admin 401 /who {"확인됨":true, "이름":"hong", …} ← 정작 도착한 코드에서는 확인된 상태입니다

가장 아래 두 줄이 이 함정을 어렵게 만듭니다. 인가는 인증보다 먼저 지나므로 그 시점에는 아무도 아닌 상태로 보고 401을 냅니다. 그런데 인증은 그 뒤에 제대로 동작하므로, 통과한 곳에서는 로그인한 것으로 보입니다.

오류도 경고도 나지 않습니다. "로그인이 되는데 계속 로그인하라고 한다" 는 증상으로 나타납니다. 그때는 Program.cs 의 두 줄 순서를 먼저 보십시오.

버전 배지

인증 방식은 여럿을 함께 둘 수 있습니다

스킴

AddCookie·AddJwtBearer 처럼 방식마다 이름(스킴)이 있습니다. 화면은 쿠키로, API 는 토큰으로 두고 한 앱에 함께 둘 수 있습니다. 어트리뷰트에 이름을 적어 고릅니다.

AddAuthentication("이름") 의 그 이름이 기본 스킴입니다. 적지 않으면 기본 스킴이 쓰입니다. 4.3에서 토큰 방식을 다룹니다.

실습 문제

직접 해보기

1. 클레임 더해 보기 난이도 하

위 실행 예제에 부서 클레임을 더하고, 영업부인지 확인하는 줄을 적어보세요.

종류 이름은 우리가 정해도 됩니다. 찾을 때는 그 이름을 그대로 넘깁니다.
new Claim("부서", "영업") Console.WriteLine(사람.HasClaim("부서", "영업")); // True Console.WriteLine(사람.FindFirst("부서")?.Value); // 영업
2. 401 과 403 선택하기 난이도 중

아래 넷에 어느 코드가 맞는지 적어보세요.

① 쿠키가 없습니다.
② 쿠키가 있는데 기한이 지났습니다.
③ 로그인했지만 남의 글을 지우려 합니다.
④ 로그인했고 자기 글인데, 이미 지워진 글입니다.
기한이 지난 쿠키로는 누구인지 알 수 없습니다. ④ 는 권한 문제가 아닙니다.
① 401 누구인지 모릅니다. ② 401 같은 까닭입니다. 다시 로그인하면 풀립니다. ③ 403 누구인지는 압니다. 다시 로그인해도 소용없습니다. ④ 404 인증도 인가도 지났습니다. 없는 것을 가리킨 것뿐입니다. // 다만 ③ 을 404 로 두는 설계도 있습니다. 403 을 돌려주면 // "그 글이 있기는 하다"는 것을 알려 주기 때문입니다.
3. 역할을 바꿨는데 그대로일 때 난이도 상

관리자 화면에서 어떤 회원의 역할을 admin 으로 올렸는데, 그 사람은 여전히 403 을 받습니다. 다시 로그인하면 됩니다. 왜 그런지, 그리고 다시 로그인하지 않고도 되게 하려면 어떻게 할지 적어보세요.

인가는 무엇을 보고 판정한다고 했습니까. 그 값은 언제 만들어졌습니까.
// 인가는 쿠키에 담긴 클레임만 봅니다. 그 클레임은 로그인하던 // 시점의 사본이라, 데이터베이스를 고쳐도 바뀌지 않습니다. // 다시 로그인하면 새 클레임이 담겨 풀립니다. // 다시 로그인하지 않고 되게 하려면 둘 중 하나입니다. // 1. 쿠키를 다시 발급합니다(SignInAsync 를 다시 부릅니다). // 역할을 바꾼 그 자리에서 해 주면 됩니다. // 2. 보안 스탬프처럼 "바뀌었음"을 알리는 값을 두고, 일정 간격마다 // 또는 그 자리에서 대조해 어긋나면 쿠키를 다시 만듭니다. // 값이 그때그때 정확해야 한다면 클레임에 담지 말고 필요할 때 // 읽는 편이 낫습니다. 클레임은 자주 바뀌지 않는 것에 맞습니다.
요약
  • 인증은 누구인지(실패하면 401), 인가는 해도 되는지(실패하면 403)입니다.
  • UseAuthentication()먼저입니다. 뒤바꾸면 로그인해도 401 이 나고 오류는 없습니다.
  • 인가가 보는 것은 쿠키에 담긴 클레임입니다. 데이터베이스를 다시 읽지 않습니다.
  • 역할로 모자라면 정책을 만듭니다. 역할과 무관하게 판정합니다.
  • 클레임은 로그인 시점의 사본이라, 그 뒤에 바뀐 것은 다시 로그인해야 반영됩니다.