-> 위 3단계 과정을 구현해보기.
npm install express jsonwebtoken body-parser
JWT (JSON Web Token)를 생성하고 검증하는 Node.js 라이브러리.
jwt.sign(payload, secret, options) - JWT를 생성하는 메서드.
- payload: 토큰에 포함할 데이터(예: 사용자 ID, 역할 등).
- secret: 서버에서 사용하는 비밀 키.
- options (선택): 토큰 만료 시간(expiresIn) 등 설정.
jwt.verify(token, secret, callback) - JWT를 검증하는 메서드.
- token: 클라이언트가 보낸 JWT.
- secret: JWT를 생성할 때 사용한 비밀 키.
- callback: 검증 결과를 처리하는 함수.
jwt.decode(token) - JWT를 디코딩하여 페이로드를 읽는 메서드.
Express.js에서 요청(Request) 바디를 파싱하는 미들웨어.
클라이언트가 보낸 데이터를 JSON 또는 URL-encoded 형식으로 서버에서 쉽게 읽을 수 있도록 변환해주는 역할을 수행.
단, Express.js 4.16.0 이후 버전에서는 body-parser가 Express에 내장되어있어서 사용이 불필요하다.
const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');
const app = express();
const PORT = 3000;
// JWT Secret Key
const SECRET_KEY = 'your_secret_key';
// Body parser 설정
app.use(bodyParser.json());
// Mock 사용자 데이터
const users = [
{ id: 1, username: 'alice', password: 'password123' },
{ id: 2, username: 'bob', password: 'mypassword' },
];
// 로그인 라우트 (JWT 생성)
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 사용자 확인
const user = users.find(
(u) => u.username === username && u.password === password
);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// JWT 생성
const token = jwt.sign({ id: user.id, username: user.username }, SECRET_KEY, {
expiresIn: '1h', // 토큰 유효 기간: 1시간
});
res.json({ token });
});
// JWT 인증 미들웨어
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
// 헤더에서 토큰 추출 (Bearer 방식)
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(403).json({ message: 'Token is required' });
}
// 토큰 검증
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
// 유저 정보를 요청 객체에 추가
req.user = user;
next();
});
};
// 보호된 경로
app.get('/protected', authenticateToken, (req, res) => {
res.json({ message: `Hello, ${req.user.username}! Welcome to the protected route.` });
});
// 서버 시작
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
사용자가 POST /login으로 사용자 이름과 비밀번호를 백엔드로 전송한다.
서버는 사용자 정보를 확인하고, 유효한 사용자라면 JWT를 생성하여 반환한다.
JWT에는 무엇이 포함되어야 하는가?
jwt.sign(payload, secret, options)
- payload: JWT에 포함될 데이터(예: 사용자 ID).
- secret: 서버에서 JWT의 유효성을 검증하는 데 사용할 비밀 키.
- options: 토큰 만료 시간 등 설정.
프론트엔드는 검증이 필요한 모든 로직에 반환받은 JWT를 동봉하여 백엔드로 전송한다.
백엔드에서는 전송된 JWT가 올바른 것인지 검증한다.
(예제 코드에서는) 인증이 필요한 엔드포인트(/protected)는 검증 미들웨어를 통해 JWT를 검증한다.
검증 미들웨어는 JWT가 유효하면, req.user에 디코딩된 사용자 정보를 추가하고 다음으로 넘어간다.
요청 URL: POST
http://localhost:3000/login
{
"username": "alice",
"password": "password123"
}
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhbGljZSIsImlhdCI6MTY4ODAwMjAwMCwiZXhwIjoxNjg4MDA1NjAwfQ.kHR6J14Kk6P_J1RG7PzHQl7brERnsCRN3l8l8K5dSTU"
}
요청 URL: GET
http://localhost:3000/protected
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhbGljZSIsImlhdCI6MTY4ODAwMjAwMCwiZXhwIjoxNjg4MDA1NjAwfQ.kHR6J14Kk6P_J1RG7PzHQl7brERnsCRN3l8l8K5dSTU
SECRET_KEY는 .env 파일로 분리하고, 환경 변수로 관리.
Secure 속성을 활성화하여 네트워크에서 토큰 탈취를 방지.
로그아웃 시 사용된 JWT를 블랙리스트에 등록하여 무효화.
expiresIn을 짧게 설정하고, 필요하면 리프레시 토큰(refresh token)을 사용.
1차 목표에서 사용했던 JWT는 액세스 토큰(Access Token)이라고 한다. API 요청 인증에 사용할 목적.
그런데 1번 검증을 통과했다고 천년만년 대문을 열어줄 수는 없는 노릇이다.
1차로 생성된 액세스 토큰(Access Token)의 유효기간이 짧을 수록, 더 많은 검증을 받는다는 뜻이고, 이건 보안이 더욱 강화된다는 뜻이다.
문제는 액세스 토큰(Access Token)를 많이 발급받을 수록, 서버 리소스 사용량도 늘어나고 작업이 늘어지면서 사용자에게도 그리 좋지 못한 경험을 주게된다.
이럴 때 사용하는 것이 리프레시 토큰(refresh token). 리프레시 토큰(Refresh Token)은 액세스 토큰(Access Token)의 수명을 연장하기 위해 사용하는 별도의 토큰이다.
특징 | 액세스 토큰(Access Token) | 리프레시 토큰(Refresh Token) |
---|---|---|
사용 목적 | API 요청 인증 | 새로운 액세스 토큰 발급 |
유효 기간 | 짧음 (15분 ~ 1시간) | 김 (7일 ~ 30일 이상) |
저장 위치 | 클라이언트 (LocalStorage, Cookie) | 클라이언트(보안 강화) 또는 서버 저장 |
보안 고려 사항 | 탈취 시 악용 가능 | 탈취 시 재발급 방지 조치 필요 |
서버 요청 시 포함 | Authorization 헤더 | 별도의 리프레시 엔드포인트에서 사용 |