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

JWT 기반 인증

토큰으로 신분을 나릅니다.

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

서버가 기억하지 않는 방식

4.2의 쿠키 방식은 서버가 쿠키를 만들어 보내고, 그것이 돌아오면 풀어 읽었습니다. JWT 는 그 자리에 글자 하나를 사용합니다. 담긴 내용이 토큰 안에 그대로 있어, 서버는 아무것도 기억하지 않고 받은 것만 보고 판정합니다.

쿠키JWT
보내는 자리브라우저가 알아서Authorization 머리글에 직접
서버가 기억하나기억하지 않아도 됩니다기억하지 않습니다
다른 도메인설정이 까다롭습니다상관없습니다
앱·모바일브라우저가 아니면 불편합니다맞습니다
내보내기(로그아웃)쿠키를 지우면 됩니다어렵습니다 — 아래에서 봅니다

화면이 있는 사이트는 쿠키가 편하고, API 를 앱이나 다른 사이트가 부르는 경우에 JWT 가 맞습니다. 한 앱에 둘 다 둘 수 있습니다(4.2의 스킴).

최소 예제

발급하고 검증하기

Program.cs
var 키 = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(설정에서읽은키));

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o => {
        o.TokenValidationParameters = new() {
            ValidIssuer = "mayya",          // 누가 만든 것인지
            ValidAudience = "mayya-app",    // 누구에게 준 것인지
            IssuerSigningKey = 키,           // 서명을 맞춰 볼 키
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true          // 기한이 지났는지
        };
    });

// 발급 — 로그인 확인을 마친 뒤에 부릅니다
var token = new JwtSecurityToken(
    issuer: "mayya",
    audience: "mayya-app",
    claims: [new Claim("name", name), new Claim("role", role)],
    expires: DateTime.UtcNow.AddMinutes(30),
    signingCredentials: new(키, SecurityAlgorithms.HmacSha256));

return new JwtSecurityTokenHandler().WriteToken(token);
받은 상태 코드
/mine /admin 토큰 없이 401 401 hong (user) 토큰으로 200 403 kim (admin) 토큰으로 200 200

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

401과 403의 뜻은 4.2와 같습니다. 달라진 것은 신분을 어디서 읽느냐뿐입니다. 부르는 쪽은 머리글에 담아 보냅니다.

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs…" \
     http://localhost:5000/mine
상세 사용법

토큰 안에 무엇이 들었나

점 두 개로 나뉜 세 조각입니다. 앞의 둘은 Base64Url 로 담은 JSON 이고 마지막이 서명입니다. 뜯어 보는 데에는 키도 서버도 필요 없습니다.

Program.cs
using System.Text;
using System.Text.Json;

var 토큰 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
    ".eyJuYW1lIjoiaG9uZyIsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzk4Njc1MjAwLCJpc3MiOiJtYXl5YSIsImF1ZCI6Im1heXlhLWFwcCJ9" +
    ".vvybDwKljWFwHZen_rJT7EsoDOKhoHIXL5qAmQNUzKw";

var 조각 = 토큰.Split('.');
Console.WriteLine("조각 " + 조각.Length + "개");
foreach (var s in 조각) Console.WriteLine("  " + s.Length + "자");
Console.WriteLine();

string 풀기(string s) {
    // Base64Url 은 - 와 _ 를 사용하고 = 를 떼어 둡니다. 되돌려 놓습니다.
    s = s.Replace('-', '+').Replace('_', '/');
    s = s.PadRight(s.Length + (4 - s.Length % 4) % 4, '=');

    return Encoding.UTF8.GetString(Convert.FromBase64String(s));
}

Console.WriteLine("머리 : " + 풀기(조각[0]));
Console.WriteLine("본문 : " + 풀기(조각[1]));
Console.WriteLine();

var 본문 = JsonDocument.Parse(풀기(조각[1])).RootElement;
Console.WriteLine("이름   : " + 본문.GetProperty("name").GetString());
Console.WriteLine("역할   : " + 본문.GetProperty("role").GetString());

var 만료 = DateTimeOffset.FromUnixTimeSeconds(본문.GetProperty("exp").GetInt64());
Console.WriteLine("만료   : " + 만료.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss") + " UTC");
출력
조각 3개 36자 104자 43자 머리 : {"alg":"HS256","typ":"JWT"} 본문 : {"name":"hong","role":"user","exp":1798675200,"iss":"mayya","aud":"mayya-app"} 이름 : hong 역할 : user 만료 : 2026-12-31 00:00:00 UTC

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

키 없이 본문을 그대로 읽었습니다. 이것이 JWT 를 다룰 때 가장 먼저 알아야 할 것입니다. 서명은 고치지 못하게 막는 것이지 감추는 것이 아닙니다. Base64 는 암호가 아니라 담는 방식일 뿐입니다.

그러므로 토큰에 감출 것을 담지 마십시오. 주민등록번호나 내부 식별자처럼 남이 보면 안 되는 것은 넣지 않습니다. 넣어야 한다면 그것은 토큰이 아니라 서버가 들고 있어야 합니다.

상세 사용법

어긋나면 까닭을 머리글로 알려 줍니다

검증에 실패하면 401 입니다. 본문은 비어 있지만 WWW-Authenticate 머리글에 까닭이 적혀 옵니다.

받은 응답
// 토큰의 마지막 한 글자를 고쳐 보냈을 때
401
WWW-Authenticate: Bearer error="invalid_token",
                  error_description="The signature key was not found"

// 기한이 지난 토큰을 보냈을 때
401
WWW-Authenticate: Bearer error="invalid_token",
                  error_description="The token expired at '08/30/2026 02:49:03'"
읽는 법
401 만 보고 "로그인하라" 로 넘기면 원인을 알 수 없습니다. 부르는 쪽에서 이 머리글을 읽으면 다시 로그인할지, 토큰만 새로 받으면 되는지 갈라 처리할 수 있습니다.

한 글자만 고쳐도 걸립니다. 서명은 머리와 본문 전체로 만들어지므로, 본문을 고치면 서명과 어긋납니다. 역할을 user 에서 admin 으로 바꿔 봐야 통과하지 못합니다.

다만 그것은 키를 모를 때의 이야기입니다. 키가 새면 누구나 원하는 내용의 토큰을 만들 수 있습니다. 키는 코드에 적지 않습니다(3.2에서 본 연결 문자열과 같습니다). 4.4에서 다시 봅니다.

상세 사용법

내보내기가 어렵습니다

쿠키는 지우면 끝입니다. JWT 는 서버가 아무것도 기억하지 않는 것이 장점인 만큼, 이미 나간 토큰을 거둘 방법도 없습니다. 기한이 다할 때까지는 유효합니다.

그래서 이렇게 다룹니다.

  • 기한을 짧게 둡니다. 몇 분에서 한 시간쯤입니다. 새면 그동안만 위험합니다.
  • 짧은 기한이 불편하므로 갱신 토큰을 따로 둡니다. 이쪽은 기한이 길고, 서버가 기억합니다. 그래야 거둘 수 있습니다.
  • 지금 당장 막아야 한다면 거절 목록을 둡니다. 기억하지 않는다는 장점을 그만큼 내놓는 것입니다.

4.2에서 본 "역할을 올렸는데 그대로" 도 여기서 더 심해집니다. 클레임이 토큰 안에 박혀 있어 기한이 다하기 전에는 바꿀 수 없습니다. 자주 바뀌는 값을 토큰에 담지 마십시오.

버전 배지

키는 얼마나 길어야 하나

HS256

HMAC-SHA256 으로 서명하려면 키가 256비트(32바이트) 이상이어야 합니다. 짧으면 발급할 때 예외가 발생합니다. 한글은 한 자가 3바이트이므로 글자 수로 세면 어긋납니다.

서명 방식은 두 가지입니다. HS256 은 같은 키로 만들고 맞춰 보므로 검증하는 쪽도 키를 알아야 합니다. 서버가 여럿이면 그만큼 키가 퍼집니다. RS256 은 개인 키로 만들고 공개 키로 맞춰 보므로 검증하는 쪽에 비밀을 주지 않아도 됩니다.

실습 문제

직접 해보기

1. 만료까지 남은 시간 난이도 하

위 실행 예제에 지금부터 만료까지 며칠 남았는지 찍는 줄을 더해보세요.

DateTimeOffset 끼리 빼면 TimeSpan 이 나옵니다.
var 남음 = 만료 - DateTimeOffset.UtcNow; Console.WriteLine("남음 : " + (int)남음.TotalDays + "일"); Console.WriteLine("지났나 : " + (남음 < TimeSpan.Zero));
2. 담을 것과 담지 말 것 난이도 중

아래 넷 가운데 토큰에 담아도 되는 것을 가려내고 까닭을 적어보세요.

① 회원 번호
② 전자 메일 주소
③ 비밀번호 해시
④ 지금 장바구니에 담긴 물품 수
두 가지를 함께 보십시오. 남이 읽어도 되는가, 그리고 기한 안에 바뀌는가.
① 담아도 됩니다. 남이 봐도 되고 바뀌지 않습니다. ② 대개 담습니다. 다만 누구나 읽는다는 것을 알고 담습니다. ③ 담으면 안 됩니다. 서명은 감추지 않습니다. 그대로 새어 나갑니다. ④ 담으면 안 됩니다. 읽혀서가 아니라 계속 바뀌기 때문입니다. 토큰은 발급 시점의 사본이라 물품을 담아도 숫자가 그대로입니다.
3. 내보냈는데 들어옵니다 난이도 상

어떤 회원을 탈퇴 처리했는데 그 사람의 요청이 계속 200을 받습니다. 왜 그런지, 그리고 어떻게 막을지 두 가지 이상 적어보세요.

검증할 때 데이터베이스를 읽습니까. 무엇만 보고 판정합니까.
// 검증은 토큰 안의 것과 서명·기한만 봅니다. 회원 표를 읽지 // 않으므로 탈퇴한 것을 알 수 없습니다. 기한이 다할 때까지 유효합니다. // 1. 기한을 짧게 둡니다(몇 분). 그동안만 열려 있습니다. // 갱신 토큰은 서버가 기억하므로 그 자리에서 거둡니다. // 2. 거절 목록을 둡니다. 탈퇴한 사람의 토큰 식별자(jti)를 담아 두고 // 검증할 때 함께 봅니다. 기억하지 않는다는 장점을 내놓는 것입니다. // 3. 4.2의 보안 스탬프처럼 "언제부터 무효" 를 회원마다 두고, // 토큰의 발급 시각(iat)이 그보다 앞서면 거절합니다. // 어느 쪽이든 서버가 무언가를 기억하게 됩니다. 그것이 이 방식의 // 대가입니다.
요약
  • JWT 는 신분을 토큰 안에 담아 서버가 기억하지 않게 합니다. Authorization: Bearer 로 보냅니다.
  • 본문은 키 없이도 누구나 읽습니다. 서명은 고치지 못하게 막을 뿐 감추지 않습니다.
  • 검증 실패의 까닭은 WWW-Authenticate 머리글에 옵니다.
  • 이미 나간 토큰은 거둘 수 없습니다. 기한을 짧게 두고 갱신 토큰을 함께 사용합니다.
  • 401·403의 뜻은 4.2와 같습니다. 달라진 것은 신분을 어디서 읽느냐뿐입니다.