JWT 기반 인증
토큰으로 신분을 나릅니다.
서버가 기억하지 않는 방식
4.2의 쿠키 방식은 서버가 쿠키를 만들어 보내고, 그것이 돌아오면 풀어 읽었습니다. JWT 는 그 자리에 글자 하나를 사용합니다. 담긴 내용이 토큰 안에 그대로 있어, 서버는 아무것도 기억하지 않고 받은 것만 보고 판정합니다.
| 쿠키 | JWT | |
|---|---|---|
| 보내는 자리 | 브라우저가 알아서 | Authorization 머리글에 직접 |
| 서버가 기억하나 | 기억하지 않아도 됩니다 | 기억하지 않습니다 |
| 다른 도메인 | 설정이 까다롭습니다 | 상관없습니다 |
| 앱·모바일 | 브라우저가 아니면 불편합니다 | 맞습니다 |
| 내보내기(로그아웃) | 쿠키를 지우면 됩니다 | 어렵습니다 — 아래에서 봅니다 |
화면이 있는 사이트는 쿠키가 편하고, API 를 앱이나 다른 사이트가 부르는 경우에 JWT 가 맞습니다. 한 앱에 둘 다 둘 수 있습니다(4.2의 스킴).
발급하고 검증하기
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);
이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다.
401과 403의 뜻은 4.2와 같습니다. 달라진 것은 신분을 어디서 읽느냐뿐입니다. 부르는 쪽은 머리글에 담아 보냅니다.
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs…" \
http://localhost:5000/mine
토큰 안에 무엇이 들었나
점 두 개로 나뉜 세 조각입니다. 앞의 둘은 Base64Url 로 담은 JSON 이고 마지막이 서명입니다. 뜯어 보는 데에는 키도 서버도 필요 없습니다.
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");
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
키 없이 본문을 그대로 읽었습니다. 이것이 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'"
한 글자만 고쳐도 걸립니다. 서명은 머리와 본문 전체로 만들어지므로, 본문을 고치면 서명과 어긋납니다. 역할을 user 에서 admin 으로 바꿔 봐야 통과하지 못합니다.
다만 그것은 키를 모를 때의 이야기입니다. 키가 새면 누구나 원하는 내용의 토큰을 만들 수 있습니다. 키는 코드에 적지 않습니다(3.2에서 본 연결 문자열과 같습니다). 4.4에서 다시 봅니다.
내보내기가 어렵습니다
쿠키는 지우면 끝입니다. JWT 는 서버가 아무것도 기억하지 않는 것이 장점인 만큼, 이미 나간 토큰을 거둘 방법도 없습니다. 기한이 다할 때까지는 유효합니다.
그래서 이렇게 다룹니다.
- 기한을 짧게 둡니다. 몇 분에서 한 시간쯤입니다. 새면 그동안만 위험합니다.
- 짧은 기한이 불편하므로 갱신 토큰을 따로 둡니다. 이쪽은 기한이 길고, 서버가 기억합니다. 그래야 거둘 수 있습니다.
- 지금 당장 막아야 한다면 거절 목록을 둡니다. 기억하지 않는다는 장점을 그만큼 내놓는 것입니다.
4.2에서 본 "역할을 올렸는데 그대로" 도 여기서 더 심해집니다. 클레임이 토큰 안에 박혀 있어 기한이 다하기 전에는 바꿀 수 없습니다. 자주 바뀌는 값을 토큰에 담지 마십시오.
키는 얼마나 길어야 하나
HMAC-SHA256 으로 서명하려면 키가 256비트(32바이트) 이상이어야 합니다. 짧으면 발급할 때 예외가 발생합니다. 한글은 한 자가 3바이트이므로 글자 수로 세면 어긋납니다.
서명 방식은 두 가지입니다. HS256 은 같은 키로
만들고 맞춰 보므로 검증하는 쪽도 키를 알아야 합니다. 서버가
여럿이면 그만큼 키가 퍼집니다. RS256 은 개인
키로 만들고 공개 키로 맞춰 보므로 검증하는 쪽에 비밀을 주지 않아도 됩니다.
직접 해보기
위 실행 예제에 지금부터 만료까지 며칠 남았는지 찍는 줄을 더해보세요.
DateTimeOffset 끼리 빼면 TimeSpan 이 나옵니다.아래 넷 가운데 토큰에 담아도 되는 것을 가려내고 까닭을 적어보세요.
① 회원 번호 ② 전자 메일 주소 ③ 비밀번호 해시 ④ 지금 장바구니에 담긴 물품 수
어떤 회원을 탈퇴 처리했는데 그 사람의 요청이 계속 200을 받습니다. 왜 그런지, 그리고 어떻게 막을지 두 가지 이상 적어보세요.
- JWT 는 신분을 토큰 안에 담아 서버가 기억하지 않게 합니다.
Authorization: Bearer로 보냅니다. - 본문은 키 없이도 누구나 읽습니다. 서명은 고치지 못하게 막을 뿐 감추지 않습니다.
- 검증 실패의 까닭은
WWW-Authenticate머리글에 옵니다. - 이미 나간 토큰은 거둘 수 없습니다. 기한을 짧게 두고 갱신 토큰을 함께 사용합니다.
- 401·403의 뜻은 4.2와 같습니다. 달라진 것은 신분을 어디서 읽느냐뿐입니다.