JWT (JSON Web Token) 완벽 가이드: 개념부터 실전 적용까지
안녕하세요! 오늘은 웹 개발에서 인증과 권한 부여에 널리 사용되는 JWT(JSON Web Token)에 대해 알아보겠습니다. 초보 개발자도 쉽게 이해할 수 있도록 기초부터 실제 적용 방법까지 단계별로 설명해 드릴게요.
목차
- JWT란 무엇인가?
- JWT의 구조
- JWT의 작동 원리
- JWT vs 세션 기반 인증
- JWT 구현하기 (Node.js 예제)
- JWT 보안 고려사항
- 자주 묻는 질문 (FAQ)
#1. JWT란 무엇인가?
JWT(JSON Web Token)는 당사자 간에 정보를 안전하게 전송하기 위한 개방형 표준(RFC 7519)입니다. 이 정보는 디지털 서명되어 있어 신뢰할 수 있으며, 서명은 비밀 키(HMAC 알고리즘) 또는 공개/개인 키 쌍(RSA 또는 ECDSA)을 사용하여 생성됩니다.
쉽게 설명하자면, JWT는 사용자 인증 정보를 안전하게 담아 전달할 수 있는 '디지털 증명서'라고 생각하면 됩니다.
JWT의 주요 용도:
- 인증(Authentication): 사용자가 로그인하면 서버는 JWT를 발급하고, 클라이언트는 이후 요청마다 이 토큰을 포함시켜 자신을 인증합니다.
- 정보 교환: 당사자 간에 정보를 안전하게 전송할 수 있습니다.
#2. JWT의 구조
JWT는 점(.)으로 구분된 세 부분으로 구성됩니다:

xxxxx.yyyyy.zzzzz
각 부분은 다음과 같습니다:
1. 헤더(Header)
토큰 유형과 사용된 서명 알고리즘을 지정합니다.
{
"alg": "HS256",
"typ": "JWT"
}
이 JSON 객체는 Base64Url로 인코딩되어 JWT의 첫 번째 부분을 형성합니다.
2. 페이로드(Payload)
클레임(claim)이라 불리는 엔티티와 추가 데이터를 포함합니다. 클레임은 세 가지 유형이 있습니다:
- 등록된 클레임(Registered): 미리 정의된 클레임 (iss, exp, sub, aud 등)
- 공개 클레임(Public): JWT 사용자가 정의하는 클레임
- 비공개 클레임(Private): 당사자 간 정보 공유를 위한 맞춤 클레임
{
"sub": "1234567890",
"name": "홍길동",
"iat": 1516239022,
"exp": 1516242622
}
이 페이로드도 Base64Url로 인코딩되어 JWT의 두 번째 부분을 형성합니다.
3. 서명(Signature)
인코딩된 헤더, 인코딩된 페이로드, 비밀 키, 그리고 헤더에 지정된 알고리즘을 사용하여 생성됩니다.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
서명은, 메시지가 도중에 변경되지 않았는지 확인하는 데 사용되며, 개인 키로 서명된 토큰의 경우 JWT 발신자의 신원을 확인할 수도 있습니다.
#3. JWT의 작동 원리
JWT 인증 시스템의 기본 흐름은 다음과 같습니다:
- 로그인: 사용자가 자격 증명(일반적으로 사용자 이름과 비밀번호)으로 로그인합니다.
- JWT 생성: 서버는 사용자 정보를 확인하고, 비밀 키를 사용하여 JWT를 생성합니다.
- JWT 반환: 서버는 JWT를 클라이언트에 반환합니다.
- JWT 저장: 클라이언트는 JWT를 로컬 스토리지나 쿠키에 저장합니다.
- 요청 시 JWT 전송: 클라이언트는 보호된 리소스에 접근할 때 HTTP 헤더에 JWT를 포함시켜 요청을 보냅니다.
- JWT 검증: 서버는 JWT의 서명을 검증하고, 요청을 처리합니다.

#4. JWT vs 세션 기반 인증
세션 기반 인증
- 사용자가 로그인하면 서버에 세션이 생성됩니다.
- 세션 ID가 쿠키에 저장되어 클라이언트로 전송됩니다.
- 이후 요청에서 쿠키는 자동으로 서버로 전송됩니다.
- 서버는 세션 ID를 사용하여 세션 저장소에서 사용자 정보를 확인합니다.
JWT 인증
- 사용자가 로그인하면 서버는 JWT를 생성하여 클라이언트에 반환합니다.
- 클라이언트는 JWT를 저장하고 이후 요청에 포함시킵니다.
- 서버는 JWT를 검증하고 토큰의 페이로드에서 사용자 정보를 얻습니다.
- 서버에 상태를 저장할 필요가 없습니다(stateless).
주요 차이점
특성 | 세션 기반 인증 | JWT 인증 |
서버 측 저장 | 세션 정보 저장 필요 | 저장 필요 없음 (Stateless) |
확장성 | 여러 서버 간 세션 공유 필요 | 쉽게 확장 가능 |
보안 | 서버에 세션 정보 저장 | 클라이언트에 정보 저장 (서명으로 보호) |
성능 | 세션 조회 필요 | 디코딩만 필요 |
적합한 경우 | 단일 서버 애플리케이션 | 분산 시스템, 마이크로서비스 |
#5. JWT 구현하기 (Node.js 예제)
Node.js와 Express를, jsonwebtoken 라이브러리를 사용한 간단한 JWT 구현 예제를 살펴보겠습니다.
필요한 패키지 설치
npm install express jsonwebtoken bcrypt
JWT 생성 (로그인 API)
// app.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());
// 실제로는 DB에서 가져와야 합니다
const users = [
{
id: 1,
username: 'user1',
password: '$2b$10$Ot/e1PsR5eFZcMz5a9kW7OJ7X5Zl9KnJxM1Vj0dEQMGvV1D5OTrVe', // 'password123'
role: 'user'
}
];
// 환경 변수로 관리하는 것이 좋습니다
const JWT_SECRET = 'your_jwt_secret_key';
// 로그인 라우트
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// 사용자 찾기
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ message: '사용자가 존재하지 않습니다.' });
}
// 비밀번호 확인
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });
}
// JWT 생성 (유효기간 1시간)
const token = jwt.sign(
{
id: user.id,
username: user.username,
role: user.role
},
JWT_SECRET,
{ expiresIn: '1h' }
);
// JWT 반환
res.json({
message: '로그인 성공!',
token
});
});
// JWT 검증 미들웨어
const authenticateToken = (req, res, next) => {
// 헤더에서 토큰 가져오기
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN" 형식
if (!token) {
return res.status(401).json({ message: '인증 토큰이 필요합니다.' });
}
// 토큰 검증
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ message: '유효하지 않은 토큰입니다.' });
}
req.user = user;
next();
});
};
// 보호된 라우트
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({
message: '인증된 사용자입니다!',
user: req.user
});
});
// 관리자 전용 라우트
app.get('/api/admin', authenticateToken, (req, res) => {
// 사용자 역할 확인
if (req.user.role !== 'admin') {
return res.status(403).json({ message: '관리자 권한이 필요합니다.' });
}
res.json({
message: '관리자 전용 페이지입니다!'
});
});
// 서버 시작
const PORT = 3000;
app.listen(PORT, () => {
console.log(`서버가 포트 ${PORT}에서 실행 중입니다.`);
});
클라이언트 측에서 JWT 사용 (JavaScript)
// login.js
async function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
// 로그인 요청 보내기
const response = await fetch('http://localhost:3000/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
// JWT를 로컬 스토리지에 저장
localStorage.setItem('token', data.token);
alert('로그인 성공!');
window.location.href = 'profile.html'; // 프로필 페이지로 리다이렉트
} else {
alert(`로그인 실패: ${data.message}`);
}
} catch (error) {
console.error('로그인 중 오류 발생:', error);
alert('로그인 중 오류가 발생했습니다.');
}
}
// profile.js
async function getProfile() {
// 로컬 스토리지에서 토큰 가져오기
const token = localStorage.getItem('token');
if (!token) {
alert('로그인이 필요합니다!');
window.location.href = 'login.html';
return;
}
try {
// 프로필 데이터 요청
const response = await fetch('http://localhost:3000/api/profile', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (response.ok) {
// 프로필 데이터 표시
document.getElementById('username').textContent = data.user.username;
document.getElementById('role').textContent = data.user.role;
} else {
// 토큰이 유효하지 않은 경우
alert(`오류: ${data.message}`);
localStorage.removeItem('token');
window.location.href = 'login.html';
}
} catch (error) {
console.error('프로필 가져오기 중 오류 발생:', error);
alert('프로필을 가져오는 중 오류가 발생했습니다.');
}
}
// 로그아웃 함수
function logout() {
localStorage.removeItem('token');
window.location.href = 'login.html';
alert('로그아웃 되었습니다.');
}
간단한 HTML 예제
<!-- login.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JWT 로그인 예제</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
button {
padding: 10px 15px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<h1>JWT 로그인</h1>
<div class="form-group">
<label for="username">사용자 이름:</label>
<input type="text" id="username" placeholder="사용자 이름 입력">
</div>
<div class="form-group">
<label for="password">비밀번호:</label>
<input type="password" id="password" placeholder="비밀번호 입력">
</div>
<button onclick="login()">로그인</button>
<script src="profile.js"></script>
</body>
</html>="login.js"></script>
</body>
</html>
<!-- profile.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>사용자 프로필</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.profile-card {
border: 1px solid #ddd;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
}
button {
padding: 10px 15px;
background-color: #f44336;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #d32f2f;
}
</style>
</head>
<body>
<h1>사용자 프로필</h1>
<div class="profile-card">
<p><strong>사용자 이름:</strong> <span id="username">로딩 중...</span></p>
<p><strong>역할:</strong> <span id="role">로딩 중...</span></p>
</div>
<button onclick="logout()">로그아웃</button>
<script src
#6. JWT 보안 고려사항
JWT를 사용할 때 주의해야 할 몇 가지 보안 고려사항이 있습니다:
1. 민감한 정보 저장 금지
JWT 페이로드는 암호화되지 않고 단지 인코딩될 뿐입니다. 따라서 비밀번호나 개인 식별 정보와 같은 민감한 데이터는 JWT에 포함시키지 마세요.
2. 토큰 만료 시간 설정
항상 토큰에 만료 시간(exp 클레임)을 설정하세요. 이렇게 하면 토큰이 무기한 유효하지 않게 됩니다.
// 1시간 후 만료되는 토큰
jwt.sign({ userId: 123 }, 'secret', { expiresIn: '1h' });
3. 적절한 서명 알고리즘 사용
가능하면 HS256보다 강력한 알고리즘(예: RS256)을 사용하는 것이 좋습니다.
4. 토큰 저장 위치
- 로컬 스토리지: XSS 공격에 취약할 수 있습니다.
- 쿠키: HttpOnly 및 Secure 플래그를 설정하여 CSRF 공격으로부터 보호할 수 있습니다.
5. 토큰 갱신 전략
refresh 토큰을 사용하여 액세스 토큰의 수명을 짧게 유지하면서 사용자 경험을 향상시킬 수 있습니다.
#7. 자주 묻는 질문 (FAQ)
Q: JWT는 어디에 저장해야 할까요?
A: 일반적으로 두 가지 방법이 있습니다:
- 로컬 스토리지: 쉽게 접근할 수 있지만 XSS 공격에 취약합니다.
- HttpOnly 쿠키: XSS 공격으로부터 더 안전하지만, CSRF 공격에 대비해야 합니다.
보안이 중요한 애플리케이션에서는 HttpOnly 쿠키 + CSRF 토큰을 사용하는 것이 좋습니다.
Q: JWT의 주요 장점은 무엇인가요?
A:
- 서버 측 세션 저장소가 필요 없음 (Stateless)
- 분산 시스템과 마이크로서비스에 적합
- 모바일 애플리케이션에서 사용하기 좋음
- 도메인 간 인증에 용이
Q: JWT와 OAuth의 차이점은 무엇인가요?
A: JWT는 토큰 형식이고, OAuth는 인증 프로토콜입니다. OAuth 2.0은 JWT를 토큰 형식으로 사용할 수 있습니다.
Q: JWT에서 사용자 정보를 업데이트하면 어떻게 되나요?
A: JWT는 발급 후에는 변경할 수 없습니다. 사용자 정보가 업데이트되면:
- 새 토큰을 발급하거나
- 토큰에는 변경되지 않는 정보만 포함하고 필요한 정보는 데이터베이스에서 조회하거나
- 짧은 유효 시간을 설정하여 자주 갱신되도록 할 수 있습니다.
Q: JWT는 항상 최선의 선택인가요?
A: 아니요. 작은 애플리케이션이나 단일 서버 애플리케이션의 경우 세션 기반 인증이 더 간단하고 적합할 수 있습니다. JWT는 분산 시스템, 마이크로서비스 아키텍처, 또는 서버리스 환경에 더 적합합니다.
결론
JWT는 현대 웹 애플리케이션에서 인증과 정보 교환을 위한 강력한 도구입니다. 특히 분산 시스템과 마이크로서비스 아키텍처에서 그 가치가 빛납니다. 이 글에서 다룬 내용을 기반으로 JWT를 효과적으로 구현하고 보안 위험을 최소화할 수 있기를 바랍니다.
JWT는 복잡해 보일 수 있지만, 기본 개념을 이해하고 적절한 라이브러리를 사용하면 쉽게 구현할 수 있습니다. 중요한 것은 보안 모범 사례를 따르고 애플리케이션의 요구 사항에 맞게 적용하는 것입니다.
긴 글 읽어주셔서 감사합니다.
끝.
'■Development■ > 《Etc》' 카테고리의 다른 글
[Etc] Windows에서 해시(Hash)로 파일 비교하는 방법 총정리 (0) | 2023.01.25 |
---|---|
[Etc] Git에서 HEAD 의미 (0) | 2022.09.28 |
[Etc] 웹사이트 주소로 IP 주소 알아내는 방법 총정리 (0) | 2020.04.08 |
[Etc] CBOR 완벽 가이드 : 효율적인 데이터 교환 형식의 모든 것 (0) | 2019.03.12 |
[Etc] CDN 완벽 가이드 : 개념부터 실전 적용까지 (0) | 2016.03.23 |