"로그인 유지는 어떻게 되는 거지?" 웹 개발을 처음 배울 때 가졌던 질문입니다. HTTP는 상태가 없는(stateless) 프로토콜인데, 서버가 어떻게 나를 기억할까요?
먼저, 세션 방식 이해하기
JWT를 이해하려면 먼저 전통적인 세션 방식을 알아야 합니다.
- 사용자가 로그인
- 서버가 세션 ID를 생성하고 서버 메모리/DB에 저장
- 세션 ID를 쿠키로 브라우저에 전달
- 이후 요청마다 쿠키가 자동 전송
- 서버가 세션 ID로 사용자 정보 조회
문제점: 서버가 모든 세션을 기억해야 합니다. 서버가 여러 대면? 세션 동기화가 필요하고, 이건 복잡합니다.
JWT: 서버가 기억하지 않아도 되는 방식
JWT(JSON Web Token)는 다른 접근법입니다. 서버가 사용자 정보를 기억하는 대신, 토큰 자체에 정보를 담아서 클라이언트에게 줍니다.
토큰에는 서버의 서명이 포함되어 있어서, 나중에 토큰을 받으면 "이거 내가 만든 게 맞네"라고 확인할 수 있습니다.
JWT의 구조: 점 두 개로 나뉜 세 부분
JWT는 이렇게 생겼습니다:
xxxxx.yyyyy.zzzzz
Header.Payload.Signature
1. Header (헤더)
토큰 타입과 서명 알고리즘 정보. Base64Url로 인코딩됩니다.
{"alg": "HS256", "typ": "JWT"}
2. Payload (페이로드)
실제 정보가 담기는 부분입니다. "클레임(claim)"이라고 불립니다.
{
"sub": "1234567890",
"name": "홍길동",
"admin": true,
"exp": 1706234567
}
⚠️ 중요: 페이로드는 암호화되지 않습니다! 누구나 디코딩해서 볼 수 있어요. 민감한 정보 넣지 마세요.
3. Signature (서명)
헤더 + 페이로드 + 비밀키를 조합해서 만든 서명입니다.
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
이 서명 덕분에 토큰이 변조되면 서버가 알 수 있습니다.
JWT 인증 흐름
- 사용자가 로그인
- 서버가 JWT 생성해서 응답
- 클라이언트가 JWT를 저장 (보통 localStorage나 httpOnly 쿠키)
- 이후 요청마다
Authorization: Bearer {token}헤더로 전송 - 서버가 서명 검증 후 payload 정보 사용
세션 vs JWT
| 세션 | JWT | |
|---|---|---|
| 저장 위치 | 서버 | 클라이언트 |
| 확장성 | 세션 동기화 필요 | 서버 간 공유 불필요 |
| 로그아웃 | 세션 삭제하면 끝 | 토큰 무효화 어려움 |
| 보안 | 서버에서 관리 | 토큰 탈취 시 위험 |
JWT 보안 주의사항
1. 만료 시간(exp) 필수
토큰에 만료 시간을 설정하세요. 영구 토큰은 위험합니다. 보통 15분~1시간.
2. Refresh Token 사용
Access Token은 짧게, Refresh Token으로 갱신하는 방식이 안전합니다.
3. HTTPS 필수
HTTP에서는 토큰이 그대로 노출됩니다.
4. localStorage vs httpOnly 쿠키
localStorage는 XSS에 취약하고, 쿠키는 CSRF에 취약합니다. httpOnly 쿠키 + CSRF 토큰 조합이 가장 안전합니다.
언제 JWT를 쓸까?
- 마이크로서비스 아키텍처 (서버 간 인증 공유)
- 모바일 앱 (쿠키 대신)
- Single Sign-On (SSO)
- API 인증
단순한 웹사이트라면 세션이 더 간단할 수 있습니다. 적재적소에 사용하세요.
클레임(Claim) 종류 상세
JWT 페이로드에 담기는 정보를 클레임이라고 합니다. 클레임은 세 가지로 나뉩니다.
등록된 클레임 (Registered Claims)
JWT 표준(RFC 7519)에서 미리 정의한 클레임입니다. 필수는 아니지만 사용이 권장됩니다.
| 클레임 | 이름 | 설명 |
|---|---|---|
| iss | Issuer | 토큰 발급자 |
| sub | Subject | 토큰 주체 (보통 사용자 ID) |
| aud | Audience | 토큰 대상자 (이 토큰을 사용할 서비스) |
| exp | Expiration | 만료 시간 (Unix 타임스탬프) |
| iat | Issued At | 발급 시간 |
| nbf | Not Before | 이 시간 이전에는 사용 불가 |
| jti | JWT ID | 토큰 고유 식별자 (재사용 방지) |
공개 클레임과 비공개 클레임
공개 클레임(Public Claims)은 IANA에 등록하거나 URI 형태로 정의해서 충돌을 방지하는 클레임입니다. 비공개 클레임(Private Claims)은 서버와 클라이언트 간 합의된 커스텀 클레임입니다. 실무에서는 비공개 클레임을 주로 사용합니다.
{
"sub": "user123", // 등록된 클레임
"exp": 1706234567, // 등록된 클레임
"role": "admin", // 비공개 클레임
"permissions": ["read", "write"] // 비공개 클레임
}
서명 알고리즘: HS256 vs RS256
JWT에서 가장 많이 사용되는 서명 알고리즘은 HS256과 RS256입니다. 둘의 차이를 이해하는 것이 중요합니다.
HS256 (HMAC + SHA-256)
대칭키 방식입니다. 토큰을 생성하는 쪽과 검증하는 쪽이 같은 비밀키를 공유합니다. 구현이 간단하고 빠릅니다. 단일 서버나 신뢰할 수 있는 서버 간에 적합합니다.
// HS256 서명 생성 HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), "my-secret-key" // 양쪽 모두 이 키를 알아야 함 )
RS256 (RSA + SHA-256)
비대칭키 방식입니다. 개인키(private key)로 서명하고, 공개키(public key)로 검증합니다. 토큰을 검증하는 쪽이 비밀키를 알 필요가 없으므로, 여러 마이크로서비스가 토큰을 검증해야 하는 환경에 적합합니다.
// RS256 흐름 인증 서버: 개인키로 서명 → JWT 발급 API 서버 A: 공개키로 검증 (개인키 모름) API 서버 B: 공개키로 검증 (개인키 모름) // 공개키가 유출돼도 위조할 수 없음
| HS256 | RS256 | |
|---|---|---|
| 방식 | 대칭키 | 비대칭키 |
| 키 관리 | 하나의 비밀키 공유 | 개인키/공개키 분리 |
| 속도 | 빠름 | 상대적으로 느림 |
| 적합한 환경 | 단일 서버 | 마이크로서비스 |
Access Token과 Refresh Token
실제 서비스에서는 JWT를 하나만 사용하지 않고, 두 종류의 토큰을 조합합니다.
Access Token은 실제 API 요청에 사용하는 토큰으로, 만료 시간을 짧게 설정합니다(보통 15분~1시간). 탈취되더라도 빨리 만료되므로 피해를 줄일 수 있습니다.
Refresh Token은 Access Token이 만료되었을 때 새 Access Token을 발급받기 위한 토큰입니다. 만료 시간이 길고(7일~30일), 보통 데이터베이스에 저장합니다. Refresh Token은 서버에서 즉시 무효화할 수 있어서, "강제 로그아웃" 기능을 구현할 수 있습니다.
토큰 갱신 플로우
- 클라이언트가 Access Token으로 API 요청
- 서버가 "토큰 만료" 응답 (401 Unauthorized)
- 클라이언트가 Refresh Token으로 새 Access Token 요청
- 서버가 Refresh Token 검증 후 새 Access Token 발급
- 클라이언트가 새 Access Token으로 원래 요청 재시도
이 과정은 보통 HTTP 인터셉터(interceptor)로 자동화합니다. 사용자는 토큰이 갱신되는 것을 의식하지 못합니다.
JWT 보안 모범 사례
JWT를 안전하게 사용하기 위한 체크리스트를 정리했습니다.
- 페이로드에 민감한 정보 넣지 않기: 비밀번호, 주민번호 같은 정보는 절대 넣지 마세요. JWT는 서명될 뿐, 암호화되지 않습니다.
- 충분히 긴 비밀키 사용: HS256에서는 최소 256비트(32자) 이상의 랜덤 문자열을 비밀키로 사용하세요.
- 알고리즘을 명시적으로 지정: 토큰 검증 시 허용할 알고리즘을 서버에서 명시적으로 지정하세요. 토큰의 alg 헤더를 그대로 신뢰하면 "none" 알고리즘 공격에 취약합니다.
- 토큰 크기 제한: JWT에 너무 많은 정보를 넣으면 토큰이 커지고, 매 요청마다 전송됩니다. 꼭 필요한 정보만 포함하세요.
- Refresh Token 로테이션: Refresh Token을 사용할 때마다 새 Refresh Token을 발급하고, 이전 것은 무효화합니다. 토큰 탈취 시 빠르게 감지할 수 있습니다.
"none" 알고리즘 공격
JWT 표준에는 서명이 없는 "none" 알고리즘이 정의되어 있습니다. 초기 JWT 라이브러리들에는 심각한 취약점이 있었습니다. 공격자가 토큰의 알고리즘을 "none"으로 변경하고 서명을 제거하면, 서버가 서명 검증 없이 토큰을 수용하는 문제였습니다.
// 공격 시나리오
원본 헤더: {"alg": "HS256", "typ": "JWT"}
변조 헤더: {"alg": "none", "typ": "JWT"}
// 페이로드를 원하는 대로 수정하고
// 서명 부분을 비워둠
변조된 토큰: eyJ...변조헤더.eyJ...변조페이로드.
현재 대부분의 JWT 라이브러리는 이 공격을 방어하지만, 토큰 검증 시 허용할 알고리즘을 명시적으로 지정하는 것이 여전히 중요합니다.
실전: Node.js에서 JWT 사용하기
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;
// 토큰 생성
function createToken(user) {
return jwt.sign(
{ sub: user.id, role: user.role },
SECRET,
{ expiresIn: '1h' }
);
}
// 토큰 검증
function verifyToken(token) {
try {
return jwt.verify(token, SECRET, {
algorithms: ['HS256'] // 알고리즘 명시!
});
} catch (err) {
return null; // 만료되었거나 변조된 토큰
}
}
세션 vs JWT: 언제 무엇을 선택할까
세션과 JWT 중 무엇이 "더 좋다"고 단정할 수는 없습니다. 상황에 따라 선택이 달라집니다.
세션이 적합한 경우: 단일 서버 환경, 즉시 로그아웃이 중요한 서비스(은행, 결제), 서버 측 렌더링(SSR) 위주의 전통적인 웹 애플리케이션. 세션은 서버에서 완전히 제어할 수 있으므로, 사용자를 즉시 강제 로그아웃시킬 수 있습니다.
JWT가 적합한 경우: 마이크로서비스 아키텍처, 여러 도메인 간 인증 공유(SSO), 모바일 앱 백엔드, 서버리스(Serverless) 환경. JWT는 서버 상태 없이 인증이 가능하므로 수평 확장이 쉽습니다.
실제로는 둘을 조합하는 경우도 많습니다. Refresh Token을 서버 데이터베이스에 저장하면 사실상 세션과 JWT의 장점을 모두 취할 수 있습니다. Access Token의 짧은 수명으로 매 요청의 DB 조회를 줄이면서, Refresh Token 관리를 통해 즉시 무효화 기능도 제공하는 방식입니다.
자주 묻는 질문
JWT는 암호화된 것인가요?
JWT는 기본적으로 서명되어 있지만 암호화되지는 않습니다. Base64URL로 인코딩된 것이므로 누구나 내용을 읽을 수 있습니다. JWE(JSON Web Encryption)를 사용하면 페이로드를 암호화할 수 있지만, 일반적인 JWT는 서명만 포함합니다.
JWT의 만료 시간은 어떻게 설정하나요?
exp(expiration) 클레임에 Unix 타임스탬프를 설정합니다. 일반적으로 액세스 토큰은 15분~1시간, 리프레시 토큰은 7일~30일로 설정합니다. 너무 길면 보안 위험이, 너무 짧으면 사용성이 저하됩니다.
JWT vs 세션 기반 인증, 어떤 것이 좋나요?
JWT는 서버 상태가 없어 확장성이 좋고 마이크로서비스에 적합합니다. 세션은 서버에서 관리하므로 즉시 무효화가 가능합니다. 단일 서버 애플리케이션에는 세션이, 분산 시스템에는 JWT가 더 적합한 경우가 많습니다.
JWT를 어디에 저장해야 하나요?
httpOnly 쿠키가 가장 안전합니다. localStorage는 XSS 공격에 취약하고, 일반 쿠키는 CSRF에 취약합니다. httpOnly + Secure + SameSite 플래그를 설정한 쿠키가 권장됩니다.
JWT 토큰이 탈취되면 어떻게 하나요?
짧은 만료 시간을 설정하고, 토큰 블랙리스트를 구현하거나, 리프레시 토큰 로테이션을 사용합니다. 토큰에 사용자의 IP나 디바이스 정보를 포함시켜 검증하는 방법도 있습니다.