이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
누구나 읽을 수 있는 토큰을 서버는 어떻게 믿나
JWT는 점(.)으로 이어진 세 부분, 곧 머리말과 내용과 서명으로 된 글자입니다. 앞의 두 부분은 암호화한 것이 아니라 표기만 바꿔 적은 것이라 누구나 읽고 고칠 수 있습니다. 서버가 그래도 이 토큰을 믿을 수 있는 까닭은 셋째 부분인 서명을 직접 다시 계산해 보기 때문입니다.
실행으로 대조 · Node.js 24의 crypto(HMAC-SHA256)로 토큰을 실제로 만들어 열여섯 가지 조합의 검증 결과를 대조했고, 화면의 자바 코드도 JDK 21에서 실행해 대조
// 요청 헤더 Authorization: Bearer <토큰>
// 토큰 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
// .eyJzdWIiOiI3Iiwicm9sZSI6IkFETUlOIiwiZXhwIjoxNzY3MjI1NjAwfQ
// .5hho4Bt2LLsypoE43dqORdbPqUDkMlzinRCY-zDKmMc
// 지금 now = 1767225000 (exp = 1767225600)
Claims verify(String token, long now) throws Exception {
String[] part = token.split("\\.", -1); // 머리말 . 내용 . 서명
if (part.length != 3) throw new BadTokenException("형식 오류");
Base64.Decoder dec = Base64.getUrlDecoder();
String header = new String(dec.decode(part[0]), UTF_8); // 키 없이 풀린다
String payload = new String(dec.decode(part[1]), UTF_8); // 키 없이 풀린다
// 1. 알고리즘: 서버가 정해 둔 HS256만 받는다
if (!"HS256".equals(field(header, "alg")))
throw new BadTokenException("허용하지 않는 알고리즘");
// 2. 서명: 앞의 두 부분으로 서버가 직접 다시 계산해 비교한다
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(serverKey, "HmacSHA256")); // 서버만 아는 키
byte[] expected = mac.doFinal((part[0] + "." + part[1]).getBytes(US_ASCII));
if (!MessageDigest.isEqual(expected, dec.decode(part[2])))
throw new BadTokenException("서명 불일치");
// 3. 만료: 지금이 exp보다 앞이어야 한다
if (now >= Long.parseLong(field(payload, "exp")))
throw new BadTokenException("만료됨");
return new Claims(field(payload, "sub"), field(payload, "role")); // 여기까지 와야 내용을 믿는다
}
// field(json, 이름)은 JSON에서 값 하나를 꺼내는 도우미다.
// 원리를 보이려고 직접 적은 코드다. 실제 서비스에서는 검증된 JWT 라이브러리를 쓴다.
화면의 토큰 글자, 서명 값, 열여섯 가지 조합의 검증 결과는 이 PC의 Node.js 24에 든 crypto(HMAC-SHA256)로 토큰을 실제로 만들고 검증해 본 결과와 대조했습니다. 화면의 자바 코드도 JDK 21에서 그대로 실행해 같은 토큰과 같은 결과가 나오는 것을 확인했습니다. 이 PC에는 JWT 라이브러리가 없어서, 검증 절차는 표준 문서를 보고 직접 적은 코드로 실행했습니다. 근거는 다음과 같습니다. 세 부분을 점으로 잇는 모양과 base64url 표기는 RFC 7515 3절과 2절, 서명을 계산하는 대상이 "첫째 부분.둘째 부분"이라는 것은 RFC 7515 5.1절, 검증 순서는 RFC 7515 5.2절과 RFC 7519 7.2절, 지금이 exp보다 앞이어야 한다는 것은 RFC 7519 4.1.4절, alg가 none이고 서명이 빈 토큰의 모양은 RFC 7519 6.1절입니다. 지금이 exp와 정확히 같을 때는 거절됩니다(직접 실행해 확인). 서버가 받을 알고리즘을 정해 두어야 한다는 것은 RFC 8725 3.1절의 권고이고, 널리 쓰이는 JWT 라이브러리가 none을 기본으로 거절한다는 것은 실행으로 확인하지 못했습니다. 실제 서버는 시계 오차를 감안해 exp에 몇 초에서 몇 분의 여유를 두기도 하고(RFC 7519 4.1.4절이 허용), iss, aud 같은 다른 클레임도 함께 확인합니다. 화면의 키와 토큰은 이 실습을 위해 만든 값입니다. 공개키 방식(RS256)을 쓰는 서버에서 alg를 HS256으로 바꿔 보내는 문제는 다루지 않았습니다.
앞 실습: 비밀번호 해시 · 다음 실습: OAuth 로그인 흐름
줄을 그은 곳은 처음 코드에서 지운 것이고, 색을 칠한 곳은 새로 쓴 것입니다.