본문 바로가기
Computer Science

JWT(JSON Web Token)의 이해와 활용: 2편

by 대박플머 2024. 10. 14.

JWT(JSON Web Token)의 이해와 활용: 1편

JWT(JSON Web Token)의 이해와 활용: 2편

JWT(JSON Web Token)의 이해와 활용: 3편

 

안녕하세요. 이전 글에서 JWT의 기본 개념과 구조, 그리고 생성 및 검증 과정에 대해 살펴보았습니다. 이번 글에서는 JWT의 장점과 단점, 그리고 보안 이슈에 대해 더 자세히 알아보겠습니다.

4. JWT의 장점

JWT는 현대 웹 개발에서 널리 사용되는 인증 방식입니다. 그 이유는 다음과 같은 여러 장점들 때문입니다.

무상태성: 세션 관리 필요 없음

JWT의 가장 큰 장점 중 하나는 무상태성(Statelessness)입니다. 전통적인 세션 기반 인증 방식과 달리, JWT를 사용하면 서버 측에서 사용자의 상태를 유지할 필요가 없습니다.

  1. 세션 기반 인증:
    • 사용자가 로그인하면 서버는 세션을 생성하고 세션 ID를 클라이언트에게 전달합니다.
    • 서버는 이 세션 정보를 메모리나 데이터베이스에 저장해야 합니다.
    • 클라이언트의 모든 요청마다 서버는 세션 저장소를 조회해야 합니다.
  2. JWT 기반 인증:
    • 사용자가 로그인하면 서버는 JWT를 생성하여 클라이언트에게 전달합니다.
    • 서버는 토큰 정보를 별도로 저장하지 않습니다.
    • 클라이언트의 요청마다 서버는 토큰의 서명만 확인하면 됩니다.

이러한 무상태성 덕분에 서버의 메모리 사용량이 줄어들고, 서버 확장이 더욱 용이해집니다.

확장성: 서버 간 상태 공유 불필요

JWT의 무상태성은 시스템의 확장성(Scalability)을 크게 향상시킵니다.

  1. 세션 기반 시스템의 확장:
    • 여러 서버를 사용할 경우, 세션 정보를 모든 서버가 공유해야 합니다.
    • 이를 위해 별도의 세션 저장소(예: Redis)를 구축하고 관리해야 합니다.
    • 서버 간 세션 동기화 문제가 발생할 수 있습니다.
  2. JWT 기반 시스템의 확장:
    • 각 서버는 독립적으로 토큰을 검증할 수 있습니다.
    • 서버 간 상태 공유가 필요 없어 시스템 구조가 단순해집니다.
    • 새로운 서버를 쉽게 추가할 수 있어 수평적 확장이 용이합니다.

이러한 특성 덕분에 JWT는 마이크로서비스 아키텍처나 클라우드 환경에서 특히 유용합니다.

정보 자체 포함: 사용자 정보를 토큰 내에 포함 가능

JWT의 또 다른 큰 장점은 토큰 자체에 정보를 포함할 수 있다는 점입니다.

1. 토큰에 정보 포함:

  • JWT의 페이로드에 사용자 ID, 권한 정보 등을 포함할 수 있습니다.
  • 서버는 데이터베이스 조회 없이도 이 정보를 바로 사용할 수 있습니다.

2. 정보 활용의 예:이 예시에서 서버는 토큰만으로 사용자의 ID와 역할을 알 수 있습니다.

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMzQsInJvbGUiOiJhZG1pbiJ9.qPl1aQ7dD5YIUB8Z5p8zGTxWhM0v0v0qqkab-FxRXnE"; 

const decoded = jwt.verify(token, secretKey); 
console.log(decoded.userId); // 1234 
console.log(decoded.role); // "admin"

3. 장점:

  • 데이터베이스 조회 횟수를 줄여 성능을 향상시킬 수 있습니다.
  • 마이크로서비스 환경에서 서비스 간 사용자 정보 공유가 용이합니다.

단, 토큰에 너무 많은 정보를 포함시키면 토큰의 크기가 커져 네트워크 부하가 증가할 수 있으므로 주의가 필요합니다.

다양한 플랫폼에서의 활용 가능성 (모바일, 웹, IoT 등)

JWT는 단순한 문자열 형태이기 때문에 다양한 플랫폼과 환경에서 쉽게 사용할 수 있습니다.

  1. 웹 애플리케이션:
    • 브라우저의 로컬 스토리지나 쿠키에 JWT를 저장하고 사용할 수 있습니다.
    • Single Page Application(SPA)에서 특히 유용합니다.
  2. 모바일 애플리케이션:
    • 네이티브 앱에서 JWT를 안전하게 저장하고 API 요청 시 사용할 수 있습니다.
    • 모바일 기기의 제한된 저장 공간과 네트워크 환경에서도 효율적으로 동작합니다.
  3. IoT 디바이스:
    • 제한된 리소스를 가진 IoT 디바이스에서도 JWT를 사용한 인증이 가능합니다.
    • 경량화된 인증 방식으로 IoT 환경에 적합합니다.
  4. 서버 간 통신:
    • 마이크로서비스 아키텍처에서 서비스 간 인증에 JWT를 활용할 수 있습니다.

이러한 다양성은 여러 플랫폼을 아우르는 통합 인증 시스템 구축을 가능하게 합니다.

5. JWT의 단점 및 보안 이슈

JWT의 여러 장점에도 불구하고, 몇 가지 주의해야 할 단점과 보안 이슈가 있습니다.

토큰 크기 문제: 네트워크 대역폭 부담

JWT는 세션 ID와 달리 자체적으로 정보를 포함하고 있어 크기가 상대적으로 큽니다.

  1. 토큰 크기 증가 요인:
    • 헤더와 페이로드의 정보량
    • Base64 인코딩으로 인한 크기 증가
  2. 문제점:
    • 매 요청마다 큰 크기의 토큰이 전송되어 네트워크 대역폭을 차지합니다.
    • 모바일 환경이나 네트워크 속도가 느린 환경에서 성능 저하가 발생할 수 있습니다.
  3. 해결 방안:
    • 토큰에 필수적인 정보만 포함시킵니다.
    • 압축 알고리즘을 사용하여 토큰 크기를 줄일 수 있습니다.
    • 필요한 경우 짧은 수명의 액세스 토큰과 긴 수명의 리프레시 토큰을 함께 사용합니다.

토큰 탈취 위험: HTTPS 사용 필수

JWT는 그 자체로 암호화되지 않기 때문에, 네트워크 상에서 탈취될 경우 심각한 보안 문제가 발생할 수 있습니다.

  1. 위험성:
    • JWT가 탈취되면 공격자는 토큰 내의 모든 정보를 읽을 수 있습니다.
    • 탈취된 토큰으로 사용자를 가장하여 시스템에 접근할 수 있습니다.
  2. 대응 방안:
    • HTTPS를 필수적으로 사용하여 통신을 암호화합니다.
    • 토큰의 유효 기간을 짧게 설정하여 탈취되더라도 피해를 최소화합니다.
    • 중요한 작업 수행 시 추가적인 인증 단계를 도입합니다.

예를 들어, 다음과 같이 Express.js에서 HTTPS를 강제할 수 있습니다:

const express = require("express");
const app = express();

app.use((req, res, next) => {
  if (!req.secure && req.get("x-forwarded-proto") !== "https") {
    return res.redirect("https://" + req.get("host") + req.url);
  }
  next();
});

만료된 토큰 관리 문제: 로그아웃 시 토큰 무효화가 어려움

JWT의 무상태성은 장점이지만, 동시에 토큰 관리의 어려움을 야기합니다.

  1. 문제점:
    • 서버에서 발급된 토큰은 만료 시간이 되기 전까지 유효합니다.
    • 사용자가 로그아웃하더라도 해당 토큰은 여전히 유효한 상태로 남아있습니다.
  2. 대응 방안:
    • 블랙리스트 관리: 로그아웃된 토큰을 블랙리스트에 추가하여 관리합니다.
    const blacklist = new Set();
    
    app.post("/logout", (req, res) => {
      const token = req.headers.authorization.split(" ")[1];
      blacklist.add(token);
      res.json({ message: "로그아웃 성공" });
    });
    
    function checkBlacklist(req, res, next) {
      const token = req.headers.authorization.split(" ")[1];
      if (blacklist.has(token)) {
        return res.status(401).json({ error: "유효하지 않은 토큰" });
      }
      next();
    }
    
    app.use(checkBlacklist);
    • 짧은 만료 시간 설정: 토큰의 유효 기간을 짧게 설정하여 리스크를 줄입니다.
    • 리프레시 토큰 사용: 액세스 토큰의 수명은 짧게, 리프레시 토큰의 수명은 길게 설정하여 관리합니다.

클라이언트에 저장된 토큰의 위험성: XSS 공격 가능성

클라이언트 측에서 JWT를 안전하게 저장하는 것은 중요한 보안 과제입니다.

  1. 위험성:
    • 로컬 스토리지나 세션 스토리지에 저장된 토큰은 XSS 공격에 취약합니다.
    • JavaScript를 통해 쉽게 접근할 수 있어 공격자가 토큰을 탈취할 수 있습니다.
  2. 대응 방안:
    • HttpOnly 쿠키 사용: JavaScript에서 접근할 수 없는 HttpOnly 쿠키에 토큰을 저장합니다.
    res.cookie("token", token, {
      httpOnly: true,
      secure: true,
      sameSite: "strict",
    });
    • 토큰 암호화: 클라이언트에서 토큰을 추가로 암호화하여 저장합니다.
    • CSP(Content Security Policy) 설정: XSS 공격을 방지하기 위한 보안 정책을 설정합니다.

JWT 보안 모범 사례

JWT를 안전하게 사용하기 위한 몇 가지 모범 사례를 살펴보겠습니다.

  1. 짧은 만료 시간 설정:
    • 액세스 토큰의 유효 기간을 짧게 설정합니다 (예: 15분 ~ 1시간).
    • 필요한 경우 리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급받습니다.
    const token = jwt.sign({ userId: user.id }, secretKey, { expiresIn: "15m" });
  2. HTTPS 필수:
    • 모든 통신에 HTTPS를 사용하여 토큰 탈취를 방지합니다.
    • 프로덕션 환경에서는 HTTPS 리다이렉션을 강제합니다.
  3. 토큰 서명에 강력한 키 사용:
    • 충분히 길고 복잡한 비밀 키를 사용합니다.
    • 가능하다면 비대칭 키 암호화(RSA)를 사용합니다.
    const fs = require('fs');
    const privateKey = fs.readFileSync('private.key');
    const token = jwt.sign({ userId: user.id }, privateKey, { algorithm: 'RS256' });
  4. 쿠키(HttpOnly, Secure) 저장 전략 활용:
    • JWT를 HttpOnly 쿠키에 저장하여 JavaScript를 통한 접근을 방지합니다.
    • Secure 플래그를 설정하여 HTTPS 연결에서만 쿠키가 전송되도록 합니다.
    • SameSite 속성을 설정하여 CSRF 공격을 방지합니다.
    res.cookie("token", token, {
      httpOnly: true,
      secure: true,
      sameSite: "strict",
    });
  5. 토큰 페이로드에 민감한 정보 포함 금지:
    • JWT의 페이로드는 암호화되지 않으므로, 비밀번호나 개인정보 등 민감한 데이터를 포함하지 않습니다.
    • 필요한 최소한의 정보만 포함시킵니다.
  6. 적절한 알고리즘 선택:
    • 안전한 알고리즘(예: HS256, RS256)을 사용합니다.
    • "none" 알고리즘 사용을 명시적으로 금지합니다.
    jwt.verify(token, secretKey, { algorithms: ["HS256", "RS256"] });
  7. 토큰 검증 철저히 수행:
    • 서명 확인뿐만 아니라 발행자(iss), 대상자(sub), 만료 시간(exp) 등 모든 관련 클레임을 검증합니다.
    jwt.verify(token, secretKey, {
      algorithms: ["HS256"],
      issuer: "your-app-name",
      audience: "your-api-name",
    });
  8. 에러 처리 주의:
    • 토큰 검증 실패 시 구체적인 에러 메시지를 반환하지 않습니다.
    • 일반적인 "인증 실패" 메시지만 반환하여 공격자에게 유용한 정보를 제공하지 않도록 합니다.
    try {
      const decoded = jwt.verify(token, secretKey);
      // 토큰이 유효한 경우의 처리
    } catch (error) {
      res.status(401).json({ error: "인증 실패" });
    }
  9. 토큰 순환(Rotation) 구현:
    • 주기적으로 새로운 토큰을 발급하여 동일한 토큰이 장기간 사용되는 것을 방지합니다.
    • 리프레시 토큰을 사용할 경우, 리프레시 토큰도 주기적으로 갱신합니다.
  10. 모니터링 및 로깅:
    • 비정상적인 토큰 사용 패턴을 모니터링합니다.
    • 토큰 발급, 사용, 갱신 등의 이벤트를 로깅하여 보안 감사에 활용합니다.

결론

JWT는 강력하고 유연한 인증 메커니즘을 제공하지만, 그 사용에는 주의가 필요합니다. 이 글에서 살펴본 JWT의 장점들 - 무상태성, 확장성, 정보 포함 능력, 다양한 플랫폼 지원 - 은 현대 웹 애플리케이션 개발에 큰 이점을 제공합니다.

그러나 동시에 토큰 크기 문제, 보안 위험, 만료된 토큰 관리의 어려움 등의 단점도 존재합니다. 이러한 단점들을 인식하고 적절한 대응 방안을 적용하는 것이 중요합니다.

JWT를 안전하게 사용하기 위해서는 HTTPS 사용, 적절한 토큰 저장 방식 선택, 강력한 암호화 키 사용, 짧은 만료 시간 설정 등의 보안 모범 사례를 따라야 합니다. 또한, 시스템의 요구사항과 보안 정책에 맞게 JWT 구현을 최적화하는 것이 필요합니다.

결국, JWT는 강력한 도구이지만, 그 효과적인 사용을 위해서는 개발자의 신중한 접근과 지속적인 보안 의식이 필요합니다. 적절히 구현된 JWT 기반 인증 시스템은 현대적이고 안전한 웹 애플리케이션 개발에 큰 도움이 될 것입니다.