한양과학기술고등학교

에듀테크 프로젝트 대시보드

JWT (JSON Web Token)

JWT — JSON Web Token

로그인에 성공하면 서버는 "이 사람은 확인된 사용자"라는 출입증(토큰)을 발급한다. JWT는 그 출입증을 헤더.페이로드.서명 세 조각을 점(.)으로 이은 문자열로 표현한다. 내용은 암호화되지 않고 그냥 Base64URL로 인코딩만 되어 누구나 읽을 수 있지만, 맨 뒤의 서명 때문에 내용을 위조하면 즉시 들통난다. 아래에서 직접 만들고 깨뜨려 보자.

라이브 디코더 & 서명기 — 페이로드를 바꾸면 서명이 다시 계산된다

HEADER — 알고리즘/타입 PAYLOAD — 클레임(주장) 데이터 SIGNATURE — HMAC-SHA256 서명
…
서명 비밀키(secret)
계산 중…

토큰 문자열을 그대로 복사해 jwt.io 같은 사이트에 붙여넣으면 페이로드가 그대로 보인다. 즉 JWT에 비밀번호·주민번호 같은 민감정보를 넣으면 안 된다. 서명은 "내용 보호"가 아니라 "위조 방지"다.

위조 실험 — role 을 student → admin 으로 바꿔 보자

위 PAYLOAD의 "role": "student" 를 "admin" 으로 고쳐 보자. 토큰 문자열은 바뀌지만, 비밀키를 모르면 서명을 다시 만들 수 없다. 아래 검증기는 서버가 하는 일을 흉내 낸다 — 받은 토큰의 앞 두 조각으로 서명을 재계산해서 토큰에 붙어온 서명과 일치하는지만 본다.

서버의 비밀키
토큰을 넣고 검증을 눌러 보자.

HS256 서명은 이렇게 만들어진다

# 1) 헤더·페이로드를 각각 JSON → Base64URL 인코딩
h = base64url(json(header))
p = base64url(json(payload))

# 2) 두 조각을 점으로 이은 뒤, 비밀키로 HMAC-SHA256
signing_input = h + "." + p
sig = base64url( HMAC_SHA256(key=secret, msg=signing_input) )

# 3) 최종 토큰 = 세 조각을 점으로 연결
token = signing_input + "." + sig

# 검증(서버): 같은 방식으로 sig 를 다시 계산해서 비교
assert HMAC_SHA256(secret, h + "." + p) == b64url_decode(sig)

HMAC(Hash-based Message Authentication Code)은 "메시지 + 비밀키"를 함께 해시해서 만든 짧은 지문이다. 비밀키가 1글자만 달라도 결과가 완전히 달라지고, 지문만 보고 비밀키를 역산할 수 없다. 그래서 비밀키를 가진 서버만 올바른 서명을 만들 수 있다.

alg: "none" 공격: 옛날 라이브러리 중엔 헤더의 alg 를 none 으로 바꾸면 서명 검증을 통째로 건너뛰는 버그가 있었다. 그래서 서버는 허용할 알고리즘을 코드에 고정하고, 클라이언트가 보낸 alg 값을 그대로 믿으면 안 된다.

JWT 로그인 흐름

1

로그인 요청 — 브라우저가 아이디·비밀번호를 서버로 보낸다.

2

토큰 발급 — 서버가 아이디를 확인하고, 사용자 정보 + exp(만료)를 담아 서명한 JWT를 돌려준다.

3

저장 — 브라우저가 토큰을 보관한다(쿠키 또는 메모리). 이후 요청마다 Authorization: Bearer <토큰> 헤더로 함께 보낸다.

4

검증 — 서버는 매 요청마다 서명·만료만 확인한다. DB 조회 불필요 — 토큰 자체가 신원 증명이라 서버가 세션을 기억하지 않아도 된다(무상태·stateless).

5

만료 — exp 가 지나면 서버가 거부한다. 보통 짧게(15분) 두고, 긴 refresh token 으로 새로 발급받는다.

서버 세션 방식과 비교

서버 세션 (쿠키+세션ID)JWT
서버가 기억하는 것세션 저장소에 로그인 상태 전부 저장비밀키 하나만. 상태는 토큰 안에
요청마다세션ID로 저장소 조회서명·만료만 계산 (조회 없음)
서버 여러 대로 확장세션 공유 저장소(Redis 등) 필요비밀키만 공유하면 끝 유리
즉시 로그아웃/차단세션 지우면 끝 쉬움만료 전엔 강제 무효화가 어려움 약점
토큰 크기세션ID 짧음페이로드만큼 커짐(매 요청 전송)

정답은 없다. 모바일 앱·여러 서버·다른 회사 API 연동엔 JWT가, 단일 웹서비스로 즉시 차단이 중요하면 세션이 편하다. 이 학교 서버의 fastapi-lms 는 세션 방식, 많은 오픈 API는 JWT(또는 그 친척인 OAuth 액세스 토큰)를 쓴다.

자주 쓰는 표준 클레임(payload 필드)

필드이름뜻
subsubject토큰의 주인 (보통 사용자 ID)
iatissued at발급 시각 (Unix time, 초)
expexpiration만료 시각. 이 시각이 지나면 무효
nbfnot before이 시각 전에는 사용 불가
ississuer토큰을 발급한 주체 (서비스 이름)
audaudience이 토큰을 받아야 할 대상
jtiJWT ID토큰 고유 번호 (재사용/블랙리스트용)

보안 체크리스트

  • 비밀키는 길고 무작위하게, 코드/깃에 넣지 말고 환경변수로. 유출되면 누구나 관리자 토큰을 만든다.
  • 서버는 허용 알고리즘을 고정(algorithms=["HS256"])하고 alg: none 을 거부한다.
  • 만료(exp)를 짧게. 탈취돼도 피해 시간이 줄어든다. 긴 세션은 refresh token으로.
  • 토큰을 localStorage에 두면 XSS 한 방에 털린다. HttpOnly 쿠키 + SameSite 가 더 안전.
  • 민감정보는 페이로드에 넣지 않는다 — 서명돼 있어도 내용은 평문이다.
  • 서명 비교는 상수 시간 비교(timing-safe)로. 라이브러리를 직접 만들지 말고 검증된 것을 쓴다.

서명 계산은 브라우저 Web Crypto API(crypto.subtle)로 이 페이지 안에서만 이뤄지며, 입력값은 서버로 전송되지 않습니다.