HS256으로 JWT 서명하기: 개발·테스트용 토큰 만들기
이 도구는 JSON 페이로드와 비밀키를 받아 HS256(HMAC-SHA256)으로 서명된 JWT를 만듭니다. API를 개발하면서 인증이 걸린 엔드포인트를 curl로 찔러 보거나, 만료 처리 로직을 테스트하려고 이미 지난 exp를 가진 토큰을 만들 때 유용합니다. 반대로 하지 말아야 할 일도 분명합니다. 운영 환경의 실제 비밀키를 여기에 넣는 것입니다.
HS256이 하는 일
HS256은 대칭키 방식입니다. base64url(header) + "." + base64url(payload) 문자열에 비밀키를 적용해 HMAC-SHA256 값을 계산하고, 그 결과를 base64url로 인코딩해 세 번째 조각으로 붙입니다. 같은 비밀키를 가진 쪽만 같은 서명을 만들 수 있으므로, 검증하는 쪽도 발급하는 쪽과 동일한 비밀키를 알아야 합니다.
이 점이 HS256의 한계이기도 합니다. 토큰을 검증해야 하는 서비스가 여러 개라면 모두가 같은 비밀키를 공유해야 하고, 그중 하나가 유출되면 전부가 위조 가능해집니다. 검증 주체가 여럿이라면 개인키로 서명하고 공개키로 검증하는 RS256이나 ES256이 적합합니다.
| 알고리즘 | 키 방식 | 적합한 상황 |
|---|---|---|
| HS256 | 비밀키 하나를 공유 | 발급자와 검증자가 같은 서비스일 때 |
| RS256 | 개인키로 서명, 공개키로 검증 | 여러 서비스가 검증, OIDC/JWKS 환경 |
| ES256 | 타원곡선 기반, 서명 길이가 짧음 | RS256과 같은 구조에 토큰 크기를 줄이고 싶을 때 |
사용 방법
- 페이로드에 넣을 JSON을 작성합니다. sub, exp 같은 표준 클레임을 쓰면 라이브러리 호환성이 좋습니다.
- 비밀키를 입력합니다. 테스트용이라면 아무 문자열이나 쓸 수 있지만, 아래의 키 길이 항목을 참고하세요.
- 생성된 토큰을 복사해 Authorization: Bearer <토큰> 헤더에 사용합니다.
- 만료 테스트가 필요하면 exp에 과거 시각의 Unix 초 값을 넣어 만료된 토큰을 만듭니다.
exp와 iat를 정확히 쓰기
exp와 iat는 UTC 기준 Unix 시각을 초 단위로 적습니다. JavaScript의 Date.now()는 밀리초를 반환하므로 1000으로 나누고 소수점을 버려야 합니다. 이 변환을 빠뜨려 exp가 1000배 큰 값이 되면, 만료가 서기 5만 년 뒤로 설정되어 사실상 영구 토큰이 됩니다. 실제로 흔히 발생하는 실수입니다.
const now = Math.floor(Date.now() / 1000);
const payload = {
sub: "user-1234",
iat: now,
exp: now + 60 * 15, // 15분 후
// 만료 테스트용이라면:
// exp: now - 60, // 1분 전 = 이미 만료
};비밀키 길이가 중요한 이유
HMAC-SHA256의 보안 강도는 비밀키의 엔트로피에 좌우됩니다. secret 이나 mysecretkey 같은 짧은 단어를 키로 쓰면, 공격자가 토큰 하나만 확보한 뒤 사전 공격으로 키를 알아낼 수 있습니다. 키를 알면 임의의 페이로드에 유효한 서명을 붙일 수 있으므로, 관리자 권한을 담은 토큰을 마음대로 만들 수 있게 됩니다.
RFC 7518은 HS256의 키가 해시 출력 길이와 같거나 그 이상, 즉 최소 256비트(32바이트)여야 한다고 규정합니다. 실무에서는 무작위 32바이트를 생성해 Base64로 표현한 값을 환경변수에 두는 방식이 일반적입니다.
# Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# OpenSSL
openssl rand -base64 32발급 시 함께 결정해야 할 것들
- 만료 시간: 액세스 토큰은 짧게(5~30분) 두고 리프레시 토큰으로 갱신하는 구조가 표준입니다.
- aud(대상): 여러 API가 같은 발급자를 신뢰한다면 aud를 넣어 토큰이 엉뚱한 API에 재사용되는 것을 막습니다.
- iss(발급자): 검증 시 발급자를 확인하면 다른 시스템의 토큰이 흘러드는 것을 차단합니다.
- jti: 일회성 토큰이나 강제 무효화가 필요하다면 고유 ID를 넣어 서버에서 추적합니다.
- 권한 정보의 양: 역할 하나 정도는 넣어도 좋지만, 세밀한 권한 목록을 넣으면 토큰이 커지고 권한 변경이 즉시 반영되지 않습니다.
자주 묻는 질문
- 여기에 운영 비밀키를 입력해도 되나요?
- 권장하지 않습니다. 서명 계산은 브라우저에서만 이루어지고 키가 네트워크로 전송되지는 않지만, 운영 서명 키는 유출되면 모든 사용자를 사칭할 수 있는 최상위 등급의 비밀입니다. 그런 값을 브라우저 입력창에 붙여넣는 습관 자체가 위험합니다. 개발용 키나 임시 키로 테스트하세요.
- 생성한 토큰이 서버에서 거부됩니다.
- 확인 순서는 이렇습니다. 첫째, 서버의 비밀키와 여기 입력한 키가 정확히 같은지(앞뒤 공백, 개행 포함). 둘째, 서버가 HS256을 허용하는지 — RS256만 허용하도록 설정된 경우가 많습니다. 셋째, exp가 이미 지났는지. 넷째, 서버가 iss나 aud를 검증하는데 페이로드에 없거나 값이 다른지. 다섯째, Authorization 헤더에 Bearer 접두사와 공백이 정확히 들어갔는지.
- 페이로드에 넣을 수 있는 데이터에 제한이 있나요?
- 형식상으로는 유효한 JSON이면 됩니다. 다만 두 가지 실질적 제약이 있습니다. 내용이 누구나 읽을 수 있으므로 비밀 값을 넣을 수 없고, 토큰이 매 요청 헤더에 실리므로 크기가 곧 비용입니다. 헤더 크기 제한(서버에 따라 4~8KB)에 걸리면 요청 자체가 거부되기도 합니다.
- RS256 토큰도 만들 수 있나요?
- 이 도구는 HS256만 지원합니다. RS256은 RSA 키 쌍이 필요하므로, RSA Key Generator 도구로 키를 만든 뒤 서버 측 라이브러리(jose, jsonwebtoken 등)로 서명하는 편이 실제 환경에 가깝습니다.
- 만료된 토큰을 일부러 만들려면?
- exp에 현재보다 작은 Unix 초 값을 넣으면 됩니다. 예를 들어 현재 시각에서 3600을 빼면 한 시간 전에 만료된 토큰이 됩니다. 클라이언트가 401을 받았을 때 리프레시 흐름을 제대로 타는지 검증할 때 유용합니다.
- 헤더의 typ이나 kid를 바꿀 수 있나요?
- 이 도구는 표준적인 {"alg":"HS256","typ":"JWT"} 헤더를 사용합니다. kid를 포함한 토큰이 필요하다면 서버 측 라이브러리에서 헤더 옵션을 지정해 발급해야 합니다. 검증 쪽 구현을 테스트할 때는 kid 값을 임의로 바꾼 토큰으로 경로 조작 방어가 되어 있는지 확인해 볼 가치가 있습니다.
💡 참고: 테스트가 끝난 임시 토큰은 버전 관리 시스템이나 이슈 트래커에 남기지 마세요. 만료가 길게 설정된 토큰이 저장소에 커밋되어 사고로 이어지는 사례가 많습니다.