인증과 인가
둘이 어떻게 다른지 구분합니다.
누구인가와 무엇을 할 수 있는가
2.5에서 401과 403을 자주 섞는다고 했습니다. 그 둘이 이 단원의 두 축입니다.
- 인증(Authentication) — 누구인지 알아냅니다. 쿠키나 토큰을 읽어 신분을 세웁니다. 실패하면 401 입니다.
- 인가(Authorization) — 그 사람이 이것을 해도 되는지 봅니다. 실패하면 403 입니다.
순서가 정해져 있습니다. 누구인지 모르면 무엇을 할 수 있는지도 알 수 없습니다. 그래서 인증이 먼저입니다.
이 사이트도 같은 방식으로 되어 있습니다. 관리자 화면은 로그인하지 않으면 401, 로그인했더라도 관리자가 아니면 403 입니다.
세 가지 문을 만들어 두고
누구나 볼 수 있는 곳, 로그인해야 하는 곳, 역할이 있어야 하는 곳을 두었습니다.
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"));
이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다.
가운데 줄이 이 단원의 전부입니다. 홍길동은 /mine 은 되는데 /admin 은 403 입니다. 누구인지는 알았으므로 401이 아닙니다. 다시 로그인하라고 해 봐야 소용없습니다.
컨트롤러에서는 어트리뷰트로 적습니다. 클래스에 붙이면 그 안의 모든 메서드에 적용됩니다.
[Authorize] // 로그인만 하면 됩니다 [Authorize(Roles = "admin")] // 역할이 있어야 합니다 [AllowAnonymous] // 위에 붙은 것을 이 메서드만 풉니다
인가가 보는 것은 클레임입니다
로그인하면 그 사람에 대해 아는 것들이 쿠키에 담깁니다. 하나하나가 클레임입니다. 인가는 이 목록만 봅니다. 그때마다 데이터베이스를 다시 읽지 않습니다.
순수한 객체라 서버 없이도 만들어 볼 수 있습니다.
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);
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
세 가지가 보입니다.
ClaimTypes.Name은 긴 주소로 풀립니다. 오래전에 정해진 이름이라 그렇습니다. 우리가 만든 나이 처럼 짧은 이름을 그대로 사용해도 됩니다.- 같은 종류를 여럿 담을 수 있습니다. 역할이 둘이면 두 줄입니다.
IsAuthenticated는 인증 방식 이름을 넘겼는지로 정해집니다. 위에서 "쿠키" 를 넘겼기에true이고, 넘기지 않은 아래쪽은false입니다.
역할로 모자랄 때는 정책으로
"관리자여야 한다" 는 역할로 되지만 "열아홉 살이 넘어야 한다" 는 되지 않습니다. 그때 정책을 만듭니다.
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("어른");
마지막 줄이 정책의 값입니다. 역할과 무관하게 판정합니다. 조건이
여럿이면 RequireClaim·RequireRole
을 이어 붙이거나, 더 복잡하면 요구 사항(Requirement)과 처리기를 따로 만듭니다.
순서를 뒤바꾸면 로그인해도 401 입니다
1.3에서 예고한 자리입니다. 실제로 뒤바꿔 보았습니다.
app.UseAuthorization(); // 먼저 두면
app.UseAuthentication();
가장 아래 두 줄이 이 함정을 어렵게 만듭니다. 인가는 인증보다 먼저 지나므로 그 시점에는 아무도 아닌 상태로 보고 401을 냅니다. 그런데 인증은 그 뒤에 제대로 동작하므로, 통과한 곳에서는 로그인한 것으로 보입니다.
오류도 경고도 나지 않습니다. "로그인이 되는데 계속 로그인하라고 한다" 는 증상으로 나타납니다. 그때는 Program.cs 의 두 줄 순서를 먼저 보십시오.
인증 방식은 여럿을 함께 둘 수 있습니다
AddCookie·AddJwtBearer
처럼 방식마다 이름(스킴)이 있습니다. 화면은 쿠키로, API 는 토큰으로 두고
한 앱에 함께 둘 수 있습니다. 어트리뷰트에 이름을 적어
고릅니다.
AddAuthentication("이름") 의 그 이름이
기본 스킴입니다. 적지 않으면 기본 스킴이 쓰입니다. 4.3에서
토큰 방식을 다룹니다.
직접 해보기
위 실행 예제에 부서 클레임을 더하고, 영업부인지 확인하는 줄을 적어보세요.
아래 넷에 어느 코드가 맞는지 적어보세요.
① 쿠키가 없습니다. ② 쿠키가 있는데 기한이 지났습니다. ③ 로그인했지만 남의 글을 지우려 합니다. ④ 로그인했고 자기 글인데, 이미 지워진 글입니다.
관리자 화면에서 어떤 회원의 역할을 admin 으로 올렸는데, 그 사람은 여전히 403 을 받습니다. 다시 로그인하면 됩니다. 왜 그런지, 그리고 다시 로그인하지 않고도 되게 하려면 어떻게 할지 적어보세요.
- 인증은 누구인지(실패하면 401), 인가는 해도 되는지(실패하면 403)입니다.
UseAuthentication()이 먼저입니다. 뒤바꾸면 로그인해도 401 이 나고 오류는 없습니다.- 인가가 보는 것은 쿠키에 담긴 클레임입니다. 데이터베이스를 다시 읽지 않습니다.
- 역할로 모자라면 정책을 만듭니다. 역할과 무관하게 판정합니다.
- 클레임은 로그인 시점의 사본이라, 그 뒤에 바뀐 것은 다시 로그인해야 반영됩니다.