웹 보안 - Express.js 기반의 백엔드에서 JWT를 사용하는 방법 (1)

이유승·2024년 12월 2일
0

웹 보안

목록 보기
5/7
post-thumbnail

1. 1차 목표

  • 사용자가 로그인 시 JWT를 생성하여 반환.
  • 클라이언트가 보호된 경로에 접근 시 JWT를 사용하여 인증.
  • 미들웨어를 통해 JWT를 검증.

-> 위 3단계 과정을 구현해보기.



2. 예제로 보는 JWT 사용방법

1. 필요한 모듈 설치

npm install express jsonwebtoken body-parser

jsonwebtoken?

  • 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를 디코딩하여 페이로드를 읽는 메서드.

body-parser?

  • Express.js에서 요청(Request) 바디를 파싱하는 미들웨어.

  • 클라이언트가 보낸 데이터를 JSON 또는 URL-encoded 형식으로 서버에서 쉽게 읽을 수 있도록 변환해주는 역할을 수행.

  • 단, Express.js 4.16.0 이후 버전에서는 body-parser가 Express에 내장되어있어서 사용이 불필요하다.



2. 서버 코드 작성

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에 디코딩된 사용자 정보를 추가하고 다음으로 넘어간다.



3. 테스트

  • 프론트엔드는 요청을 보낸다.

요청 URL: POST http://localhost:3000/login

{
    "username": "alice",
    "password": "password123"
}
  • 백엔드는 이 사용자가 유효한 사용자인지 검증하고, 유효하다면 JWT를 생성하여 반환한다.
    - JWT는 자동으로 암호화되어 통상 아래와 같은 형식으로 반환된다.
{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhbGljZSIsImlhdCI6MTY4ODAwMjAwMCwiZXhwIjoxNjg4MDA1NjAwfQ.kHR6J14Kk6P_J1RG7PzHQl7brERnsCRN3l8l8K5dSTU"
}
  • 검증이 필요한 요청이 들어온다. 이 요청에는 URL 정보와, JWT 토큰이 동봉되어있다. (요청 Header에 포함.)

요청 URL: GET http://localhost:3000/protected

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhbGljZSIsImlhdCI6MTY4ODAwMjAwMCwiZXhwIjoxNjg4MDA1NjAwfQ.kHR6J14Kk6P_J1RG7PzHQl7brERnsCRN3l8l8K5dSTU
  • 검증 미들웨어는 JWT 토큰이 유효한지 검증한다. 유효하지 않으면 요청을 거부한다.



4. 주의점

비밀 키 보호:

SECRET_KEY는 .env 파일로 분리하고, 환경 변수로 관리.

HTTPS 사용:

Secure 속성을 활성화하여 네트워크에서 토큰 탈취를 방지.

토큰 블랙리스트:

로그아웃 시 사용된 JWT를 블랙리스트에 등록하여 무효화.

짧은 만료 시간 설정:

expiresIn을 짧게 설정하고, 필요하면 리프레시 토큰(refresh token)을 사용.



3. 리프레시 토큰(refresh token)?

  • 1차 목표에서 사용했던 JWT는 액세스 토큰(Access Token)이라고 한다. API 요청 인증에 사용할 목적.

  • 그런데 1번 검증을 통과했다고 천년만년 대문을 열어줄 수는 없는 노릇이다.

  • 1차로 생성된 액세스 토큰(Access Token)의 유효기간이 짧을 수록, 더 많은 검증을 받는다는 뜻이고, 이건 보안이 더욱 강화된다는 뜻이다.

  • 문제는 액세스 토큰(Access Token)를 많이 발급받을 수록, 서버 리소스 사용량도 늘어나고 작업이 늘어지면서 사용자에게도 그리 좋지 못한 경험을 주게된다.

  • 이럴 때 사용하는 것이 리프레시 토큰(refresh token). 리프레시 토큰(Refresh Token)액세스 토큰(Access Token)의 수명을 연장하기 위해 사용하는 별도의 토큰이다.



리프레시 토큰(Refresh Token)과 액세스 토큰(Access Token)의 차이

특징액세스 토큰(Access Token)리프레시 토큰(Refresh Token)
사용 목적API 요청 인증새로운 액세스 토큰 발급
유효 기간짧음 (15분 ~ 1시간)김 (7일 ~ 30일 이상)
저장 위치클라이언트 (LocalStorage, Cookie)클라이언트(보안 강화) 또는 서버 저장
보안 고려 사항탈취 시 악용 가능탈취 시 재발급 방지 조치 필요
서버 요청 시 포함Authorization 헤더별도의 리프레시 엔드포인트에서 사용
profile
프론트엔드 개발자를 준비하고 있습니다.

0개의 댓글