Express 개발 강좌, JWT(JSON Web Token) 기반 인증 구현

오늘날 웹 애플리케이션에서 사용자 인증은 매우 중요한 요소 중 하나입니다. 특히 RESTful API를 사용하는 애플리케이션에서는 상태를 유지하지 않는 인증 방식이 필요합니다. 여기에서 JWT(JSON Web Token)는 유용한 도구로 활용됩니다. 이 강좌에서는 Node.js의 Express 프레임워크를 이용하여 JWT 기반 인증 시스템을 구현하는 방법에 대해 살펴보겠습니다. 이번 글에서는 JWT의 개념, Express 서버 설정, 사용자 등록 및 로그인 API 구현, 보호된 경로 설정 및 클라이언트 요청 처리 등을 포함하여 종합적인 내용을 다룰 것입니다.

1. JWT란 무엇인가?

JWT는 JSON Web Token의 약자로, 주로 클라이언트와 서버 간의 정보를 안전하게 전송하기 위해 사용되는 개방형 표준입니다. JWT는 세 부분으로 구성되어 있습니다:

  • Header: 토큰의 메타데이터를 포함하고 있는 부분입니다. 일반적으로 토큰의 타입(JWT)과 사용된 알고리즘(예: HMAC SHA256)을 지정합니다.
  • Payload: 인증에 필요한 정보를 담고 있는 부분으로, 사용자 ID와 같은 정보를 포함합니다. 이 데이터는 JSON 형식으로 인코딩됩니다.
  • Signature: Header와 Payload를 조합하여 서명한 부분입니다. 이를 통해 데이터가 변조되지 않았음을 확인할 수 있습니다.

1.1 JWT의 장점

JWT는 여러 가지 장점을 제공합니다:

  • 상태 비저장: 서버는 클라이언트의 상태를 저장할 필요가 없습니다.
  • 보안성: 서명 과정을 통해 나중에 토큰이 변조되지 않았음을 증명할 수 있습니다.
  • 접근성: JWT는 JSON 형식으로 되어 있어 다양한 프로그래밍 언어에서 쉽게 사용할 수 있습니다.

2. Express 서버 환경 설정

Express.js와 관련 패키지를 설치하고 기본적인 서버를 설정하는 방법을 알아보겠습니다.

2.1. Node.js 및 Express 설치

우선 Node.js가 설치되어 있어야 합니다. Node.js 공식 홈페이지 에서 설치할 수 있습니다. Node와 NPM이 설치된 후, 다음 명령어를 통해 Express와 필요한 패키지를 설치합니다:

npm init -y
npm install express jsonwebtoken bcryptjs body-parser cors

2.2. 기본 서버 설정

먼저, 기본 Express 서버를 설정해 보겠습니다. `server.js`라는 파일을 만들고 아래의 코드를 입력합니다:

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');

const app = express();
const PORT = process.env.PORT || 5000;

// 미들웨어
app.use(cors());
app.use(bodyParser.json());

// 기본 라우트
app.get('/', (req, res) => {
    res.send('Hello, Express with JWT!');
});

// 서버 시작
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

3. 사용자 등록 및 로그인 API 구현

이제 사용자 등록과 로그인을 위한 API를 구현하겠습니다. 간단한 사용자 저장소를 위해 메모리에 간단한 배열을 사용할 것입니다. 실제 애플리케이션에서는 데이터베이스를 사용하는 것이 좋습니다.

3.1. 사용자 데이터 모델

사용자 데이터를 저장하기 위해 다음과 같은 구조를 사용할 것입니다:

let users = [];

3.2. 비밀번호 해시화

사용자의 비밀번호는 저장하기 전에 안전하게 해시화해야 합니다. 이를 위해 `bcryptjs` 패키지를 사용하겠습니다. 아래는 사용자 등록 API입니다:

app.post('/api/register', async (req, res) => {
    const { username, password } = req.body;

    // 비밀번호 해시화
    const hashedPassword = await bcrypt.hash(password, 10);

    // 사용자 등록
    const newUser = { username, password: hashedPassword };
    users.push(newUser);

    res.status(201).send('User registered successfully');
});

3.3. 로그인 API 구현

로그인 API는 사용자가 제공한 정보에 따라 JWT를 생성하는 역할을 합니다:

app.post('/api/login', async (req, res) => {
    const { username, password } = req.body;
    const user = users.find(user => user.username === username);

    if (!user) {
        return res.status(400).send('Invalid credentials');
    }

    // 비밀번호 확인
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
        return res.status(400).send('Invalid credentials');
    }

    // JWT 생성
    const token = jwt.sign({ username: user.username }, 'secretKey', { expiresIn: '1h' });
    res.json({ token });
});

4. 보호된 경로 설정

JWT를 이용해 인증된 사용자만 접근 가능한 보호된 경로를 설정하겠습니다. 먼저 JWT를 검증하는 미들웨어를 작성합니다:

function authenticateToken(req, res, next) {
    const token = req.headers['authorization'] && req.headers['authorization'].split(' ')[1];

    if (!token) return res.sendStatus(401);

    jwt.verify(token, 'secretKey', (err, user) => {
        if (err) return res.sendStatus(403);
        req.user = user;
        next();
    });
}

4.1 보호된 API 예제

인증된 사용자만 접근할 수 있는 보호된 API를 만들어 보겠습니다:

app.get('/api/protected', authenticateToken, (req, res) => {
    res.send(`Hello, ${req.user.username}! This is a protected route.`);
});

5. 클라이언트 요청 처리

이제 클라이언트에서 위의 API를 호출하여 테스트해 보겠습니다. 예를 들어, `fetch`를 사용하여 사용자 등록 및 로그인 요청을 보낼 수 있습니다:

async function register(username, password) {
    const response = await fetch('http://localhost:5000/api/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password }),
    });
    return response.text();
}

async function login(username, password) {
    const response = await fetch('http://localhost:5000/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password }),
    });
    const { token } = await response.json();
    localStorage.setItem('token', token);
    return token;
}

async function accessProtected() {
    const token = localStorage.getItem('token');
    const response = await fetch('http://localhost:5000/api/protected', {
        headers: { 'Authorization': `Bearer ${token}` },
    });
    return response.text();
}

6. 결론

이제 간단한 Express 서버에서 JWT 기반 인증을 구현하는 방법을 배웠습니다. 이 과정을 통해 JWT의 개념, 사용자 등록 및 로그인 API 구현, 보호된 경로 설정 및 클라이언트 요청 처리 등을 익혔습니다. 실제 프로젝트에서는 데이터베이스와 더 많은 기능을 추가하여 발전시킬 수 있습니다. JWT를 사용하면 애플리케이션의 보안성을 높이고, 확장성을 확보할 수 있습니다. 더 나아가 OAuth와 같은 인증 체계와 연계하여 더욱 발전된 시스템을 구축할 수 있습니다.

7. 참고 자료