전통적인 웹 애플리케이션에서는 사용자의 인증 상태를 서버 측 세션에 저장하는 방식이 일반적이었습니다. 하지만 마이크로서비스 아키텍처, SPA(Single Page Application), 모바일 앱 API 등 분산 환경과 확장성이 중요한 현대 웹 개발에서 세션 방식은 여러 한계를 드러냈습니다. 서버 간 세션 공유의 복잡성, 확장성 문제, CORS(Cross-Origin Resource Sharing) 제약 등이 대표적이죠.
이러한 문제들을 해결하며 등장한 것이 바로 토큰 기반 인증, 그중에서도 JWT(JSON Web Token)입니다. JWT는 클라이언트와 서버 간의 인증 정보를 안전하게 주고받기 위한 경량의 표준 방식으로, 스테이트리스(Stateless) 아키텍처 구현에 핵심적인 역할을 합니다. 이번 포스팅에서는 JWT가 무엇인지, 어떻게 동작하며, 실제 구현 시 고려해야 할 사항들은 무엇인지 깊이 있게 알아보겠습니다.
JWT는 .을 구분자로 세 부분으로 나뉜 문자열입니다. 각 부분은 Base64Url로 인코딩되어 있으며, 디코딩하면 JSON 객체를 얻을 수 있습니다.
Header.Payload.Signature
HS256이나 RS256 같은 암호화 알고리즘이 사용됩니다.
{
"alg": "HS256",
"typ": "JWT"
}
iss (발행자), exp (만료 시간), sub (주제), aud (수신자) 등이 있습니다. 모두 선택 사항이지만, 보안과 효율성을 위해 사용하는 것이 권장됩니다.{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"exp": 1732310400 // 2024년 11월 23일 00:00:00 UTC
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
JWT 기반 인증은 다음과 같은 흐름으로 동작합니다.
Authorization 헤더(일반적으로 Bearer 스키마와 함께)에 담아 서버로 전송합니다.장점:
고려 사항 및 단점:
HttpOnly 쿠키 사용 또는 로컬 스토리지 대신 다른 안전한 저장 방식을 고려해야 합니다.Refresh Token 전략을 함께 사용하여 보안을 강화하는 것이 일반적입니다.jsonwebtoken 라이브러리)실제 백엔드에서 JWT를 발급하고 검증하는 간단한 예시를 살펴보겠습니다. Node.js 환경에서 jsonwebtoken 라이브러리를 사용하는 경우가 많습니다.
먼저, jsonwebtoken 라이브러리를 설치합니다.
npm install jsonwebtoken
JWT 발급 (로그인 시)
// authController.js
const jwt = require('jsonwebtoken');
const SECRET_KEY = 'your_super_secret_key'; // 실제 환경에서는 환경 변수로 관리하세요!
const login = (req, res) => {
const { username, password } = req.body;
// 1. 사용자 인증 로직 (예: DB에서 사용자 조회 및 비밀번호 일치 확인)
if (username === 'testuser' && password === 'password123') {
// 2. 인증 성공 시 JWT 생성
const user = { id: 'user123', username: 'testuser', role: 'admin' };
const token = jwt.sign(user, SECRET_KEY, { expiresIn: '1h' }); // 1시간 유효
return res.status(200).json({ message: 'Login successful', token: token });
} else {
return res.status(401).json({ message: 'Invalid credentials' });
}
};
module.exports = { login };
JWT 검증 (보호된 라우트 접근 시)
// middleware.js
const jwt = require('jsonwebtoken');
const SECRET_KEY = 'your_super_secret_key'; // 발급 시 사용한 것과 동일해야 합니다.
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer <token>
if (token == null) {
return res.sendStatus(401); // 토큰 없음
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
return res.sendStatus(403); // 토큰 유효하지 않음 또는 만료
}
req.user = user; // 토큰에서 추출한 사용자 정보를 request 객체에 추가
next(); // 다음 미들웨어 또는 라우트 핸들러로 이동
});
};
module.exports = { authenticateToken };
라우트 적용 예시
// app.js (Express 앱 예시)
const express = require('express');
const { login } = require('./authController');
const { authenticateToken } = require('./middleware');
const app = express();
app.use(express.json());
app.post('/api/login', login);
// 보호된 라우트 (JWT가 있어야 접근 가능)
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({ message: 'Welcome to the protected route!', user: req.user });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
이 예시를 통해 JWT가 어떻게 발급되고, 다음 요청에서 어떻게 검증되어 사용자를 인증하는지 기본적인 흐름을 이해할 수 있습니다. 실제 프로덕션 환경에서는 에러 처리, 리프레시 토큰, 보안 취약점 방지 등 더 많은 부분을 고려해야 합니다.
JWT는 현대 웹 애플리케이션의 확장성과 유연성을 극대화하는 강력한 인증 메커니즘입니다. 스테이트리스 아키텍처를 지향하고, 마이크로서비스나 SPA, 모바일 API를 개발하는 경우 JWT는 매우 매력적인 선택이 될 수 있습니다.
하지만 JWT가 만능은 아니며, 보안적 고려 사항들이 존재합니다. 토큰 탈취 위험, 무효화의 어려움 등 잠재적인 문제점들을 인지하고, 짧은 만료 시간, 리프레시 토큰 전략, 안전한 토큰 저장 방식(HttpOnly 쿠키 등)과 같은 추가적인 보안 대책을 함께 구현하는 것이 중요합니다.
JWT의 원리를 정확히 이해하고, 각 애플리케이션의 특성에 맞는 가장 안전하고 효율적인 구현 방식을 선택한다면, 더욱 견고하고 확장성 있는 서비스를 구축할 수 있을 것입니다.
Text by Chaelin & Gemini. Photographs by Chaelin, Unsplash.