본문 바로가기
IT/Network

[JSON] 보안이 강화된 JSON Web Token (JWT)

by Echinacea 2025. 2. 9.
반응형

1. JWT 개요

JSON Web Token(JWT)은 웹 애플리케이션에서 인증 및 정보 교환을 위해 널리 사용되는 토큰 기반 인증 방식입니다. 그러나 기본적인 JWT에는 보안 취약점이 존재할 수 있으므로, 보안을 강화한 JWT 사용이 중요합니다.


2. JWT의 구조

JWT는 세 부분으로 구성됩니다.

HEADER.PAYLOAD.SIGNATURE

각 부분의 역할은 다음과 같습니다:

  1. Header (헤더)
    • 토큰의 타입(JWT)과 서명에 사용할 알고리즘(예: HS256, RS256 등)을 포함합니다.
  2. Payload (페이로드)
    • 사용자 정보(클레임, claims)를 포함하며, 일반적으로 sub(사용자 ID), exp(만료 시간) 등의 정보를 가집니다.
  3. Signature (서명)
    • 토큰의 무결성을 검증하기 위해 생성된 서명입니다.
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey)

3. JWT의 사용 방식

3.1 JWT가 삽입되는 위치

JWT는 일반적으로 HTTP 요청의 다음과 같은 위치에 삽입됩니다:

1. HTTP Authorization 헤더 (가장 일반적인 방법)

Authorization: Bearer <JWT>

2. 쿠키(HttpOnly, Secure 쿠키 권장)

res.cookie('jwt', token, {
  httpOnly: true,  // JavaScript에서 접근 불가
  secure: true,    // HTTPS에서만 사용 가능
  sameSite: 'Strict', // CSRF 공격 방지
  maxAge: 3600000  // 1시간 후 만료
});

 3. URL 파라미터 (보안상 권장되지 않음)

https://example.com?token=<JWT>

4. 기본 JWT의 보안 문제

4.1 서명이 없는 JWT 사용 (alg: none)

JWT는 서명이 없으면 누구나 임의로 토큰의 내용을 변경할 수 있습니다. 따라서 alg: none 설정을 절대 사용하면 안 됩니다.

✅ 좋은 예제:

{
  "alg": "HS256",
  "typ": "JWT"
}

❌ 나쁜 예제:

{
  "alg": "none",
  "typ": "JWT"
}

4.2 비밀 키(Secret Key) 유출

비밀 키가 유출되면 누구나 JWT를 생성할 수 있어 보안이 크게 위협받습니다. 비밀 키를 환경 변수 또는 안전한 저장소에 보관해야 합니다.

 

✅ 좋은 예제:

const token = jwt.sign(payload, process.env.SECRET_KEY, { algorithm: 'HS256' });

❌ 나쁜 예제:

const token = jwt.sign(payload, 'mysecret', { algorithm: 'HS256' }); // 하드코딩된 키

4.3 만료 시간(exp) 미설정

JWT에 만료 시간(exp)이 없으면 유출된 토큰이 영구적으로 사용될 위험이 있습니다.

 

✅ 좋은 예제:

{
  "sub": "user123",
  "exp": 1700000000
}

❌ 나쁜 예제:

{
  "sub": "user123"
}

4.4 비인가된 클라이언트에서 저장

JWT를 로컬 스토리지(localStorage)에 저장하면 XSS 공격에 취약해집니다. 대신 HttpOnly, Secure 쿠키를 사용해야 합니다.

 

✅ 좋은 예제:

res.cookie('jwt', token, { httpOnly: true, secure: true, sameSite: 'Strict' });

❌ 나쁜 예제:

localStorage.setItem('jwt', token); // 로컬 스토리지 사용 (위험)


5. 보안이 강화된 JWT 적용 방법

5.1 강력한 서명 알고리즘 사용

JWT의 보안을 강화하기 위해 RS256과 같은 비대칭키 암호화 알고리즘을 사용하는 것이 권장됩니다. HS256과 같은 대칭키 암호화는 키가 유출되었을 경우 보안이 취약할 수 있습니다.

✅ 좋은 예제:

{
  "alg": "RS256",
  "typ": "JWT"
}

❌ 나쁜 예제:

{
  "alg": "none",
  "typ": "JWT"
}

5.2 토큰 만료 시간 설정

JWT는 일정 시간이 지나면 자동으로 만료되도록 설정해야 합니다. 만료 시간을 설정하지 않으면, 유출된 토큰이 계속해서 사용될 위험이 있습니다.

✅ 좋은 예제:

{
  "sub": "user123",
  "exp": 1700000000
}

❌ 나쁜 예제:

{
  "sub": "user123"
}

5.3 리프레시 토큰(Refresh Token) 사용

액세스 토큰을 짧은 유효 기간으로 설정하고, 리프레시 토큰을 이용해 새 액세스 토큰을 발급하는 방식이 보안상 안전합니다.

✅ 좋은 예제:

const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);

❌ 나쁜 예제:

const accessToken = generateAccessToken(user);
// 리프레시 토큰 없음

5.4 Secure & HttpOnly 쿠키 사용

JWT를 로컬 스토리지에 저장하면 XSS 공격에 취약할 수 있습니다. 따라서 쿠키에 저장하되 HttpOnly, Secure 옵션을 적용해야 합니다.

✅ 좋은 예제:

res.cookie('jwt', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict',
  maxAge: 3600000
});

❌ 나쁜 예제:

localStorage.setItem('jwt', token); // 로컬 스토리지 사용 (위험)

5.5 토큰 블랙리스트 사용

토큰 블랙리스트는 특정 JWT를 무효화하는 방식으로, 주로 로그아웃 처리 시 사용됩니다. 사용자가 로그아웃했을 때 기존 토큰이 재사용되지 않도록 블랙리스트를 관리해야 합니다.

토큰 블랙리스트 적용 방법:

  1. 사용자가 로그아웃할 때 해당 토큰을 블랙리스트에 추가합니다.
  2. 요청이 들어올 때 블랙리스트에 있는 토큰인지 확인 후, 존재하면 거부합니다.

✅ 좋은 예제:

blacklist.add(token);

❌ 나쁜 예제:

// 로그아웃 후에도 토큰을 무효화하지 않음

5.6 주기적으로 토큰 재발급

보안을 강화하기 위해 일정 시간이 지나면 새 토큰을 발급하고, 기존 토큰을 폐기하는 정책을 도입할 수 있습니다.

✅ 좋은 예제:

if (tokenExpired(oldToken)) {
  newToken = generateNewToken(user);
}

❌ 나쁜 예제:

// 토큰을 재발급하지 않음

6. 보안이 강화된 JWT 예제 (RS256 사용)

보안을 강화한 JWT를 발급하고 검증하는 방법을 구체적으로 살펴보겠습니다.

6.1 서버에서 JWT 발급 (Node.js, jsonwebtoken 라이브러리)

JWT를 발급할 때는 보안 강화를 위해 비대칭키(RS256) 알고리즘을 사용하는 것이 좋습니다. 이를 통해 서명 검증 시 공개키를 활용할 수 있으며, 개인키가 노출되지 않습니다.

✅ 좋은 예제:

const jwt = require('jsonwebtoken');
const fs = require('fs');

// 개인키 & 공개키 불러오기
const privateKey = fs.readFileSync('./private.pem', 'utf8');
const publicKey = fs.readFileSync('./public.pem', 'utf8');

// JWT 생성
const token = jwt.sign(
  { sub: 'user123', exp: Math.floor(Date.now() / 1000) + 60 * 60 }, // 1시간 만료
  privateKey,
  { algorithm: 'RS256' }
);

console.log(token);

❌ 나쁜 예제:

const jwt = require('jsonwebtoken');
const token = jwt.sign(
  { sub: 'user123' }, // 만료 시간 없음 (위험)
  'mysecretkey', // 비밀 키가 하드코딩됨 (위험)
  { algorithm: 'HS256' } // 비대칭키를 사용하지 않음 (위험)
);

6.2 JWT 검증 (서명 확인)

발급된 JWT가 올바른지 검증하려면, 서명을 확인해야 합니다. RS256 알고리즘을 사용하면 공개키를 이용해 검증할 수 있습니다.

✅ 좋은 예제:

try {
  const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
  console.log('Decoded:', decoded);
} catch (error) {
  console.error('Invalid token:', error.message);
}

❌ 나쁜 예제:

try {
  const decoded = jwt.verify(token, 'mysecretkey'); // 비밀 키를 직접 노출 (위험)
  console.log('Decoded:', decoded);
} catch (error) {
  console.error('Invalid token:', error.message);
}

6.3 JWT 갱신 (Refresh Token 활용)

액세스 토큰의 만료 시간이 짧을 경우, 리프레시 토큰을 활용하여 새로운 액세스 토큰을 발급하는 방법이 보안적으로 안전합니다.

JWT 갱신 과정:

  1. 클라이언트는 액세스 토큰과 리프레시 토큰을 서버에 보냅니다.
  2. 서버는 리프레시 토큰의 유효성을 확인합니다.
  3. 유효한 리프레시 토큰이라면 새로운 액세스 토큰을 생성하여 반환합니다.

✅ 좋은 예제:

const refreshToken = jwt.sign(
  { sub: 'user123', type: 'refresh' },
  privateKey,
  { algorithm: 'RS256', expiresIn: '7d' } // 7일 후 만료
);

❌ 나쁜 예제:

const refreshToken = jwt.sign(
  { sub: 'user123', type: 'refresh' },
  'mysecretkey', // 비밀 키 하드코딩 (위험)
  { algorithm: 'HS256', expiresIn: '30d' } // 너무 긴 만료 시간 (위험)
);

7. 결론

보안이 강화된 JWT를 사용하려면 서명 강화, 만료 시간 설정, 리프레시 토큰 사용, 안전한 저장 방식 적용 등의 방법을 고려해야 합니다. 또한 공개키/개인키 기반의 RS256을 사용하는 것이 더 안전한 방법입니다.

JWT는 편리한 인증 방식이지만, 보안 설정이 미흡하면 공격에 취약할 수 있으므로, 위의 보안 방법들을 적용하는 것이 중요합니다! 🔒💡

반응형

댓글