보안

JWT 기반 인증 시스템 설계: 보안 취약점 분석과 강력한 방어 전략

강코의 코딩 일기 2026. 5. 26. 14:26
반응형

JWT(JSON Web Token) 기반 인증 시스템을 안전하게 설계하고 구현하기 위한 가이드입니다. 주요 보안 취약점을 분석하고, 실용적인 방어 전략과 모범 사례를 제시하여 견고한 시스템 구축을 돕습니다.

📑 목차

JWT(JSON Web Token) 기반 인증 시스템 설계 및 구현 가이드: 보안 취약점 분석 및 방어 - firearm, revolver, bullet, gun, weapon, handgun, crime, danger, shot, shoot, crime scene, shooting, security, criminal, murder, dangerous, defense, brown security, brown gun

Image by stevepb on Pixabay

JWT 기반 인증, 왜 선택했을까? (도입)

최근 웹 및 모바일 애플리케이션 개발에서 인증 시스템은 필수적인 요소입니다. 사용자 세션을 관리하고, 상태를 유지하며, 동시에 확장성과 보안성을 확보하는 것은 언제나 개발자들의 큰 고민거리였습니다. 전통적인 세션 기반 인증 방식은 서버가 사용자 상태를 저장해야 하므로 확장성 측면에서 한계가 있었고, 특히 마이크로서비스 아키텍처나 모바일 환경에서는 더욱 복잡해지는 경향이 있었습니다.

이러한 문제에 대한 우아한 해결책 중 하나로 JWT(JSON Web Token) 기반 인증이 각광받기 시작했습니다. JWT는 토큰 자체에 사용자 정보를 담아 서버가 상태를 관리할 필요 없이 인증을 처리할 수 있게 해주는 Stateless(무상태) 방식입니다. 덕분에 서버 확장성이 뛰어나고, 여러 도메인 간 인증 처리에도 유용하며, 모바일 앱과의 연동도 매끄럽다는 장점이 있습니다. 하지만 이러한 편리함 뒤에는 간과해서는 안 될 보안 취약점들이 숨어 있습니다. 과연 우리는 JWT를 제대로 이해하고, 안전하게 사용하고 있을까요? 이 글에서는 JWT 기반 인증 시스템을 설계하고 구현할 때 마주칠 수 있는 주요 보안 문제점들을 심층적으로 분석하고, 이를 효과적으로 방어하기 위한 실용적인 전략들을 제시합니다.

JWT의 기본 구조와 동작 원리

JWT의 보안 취약점을 이해하기 위해서는 먼저 JWT가 무엇인지, 어떻게 구성되고 동작하는지 정확히 알아야 합니다. JWT는 세 부분으로 나뉘며 각 부분은 마침표(.)로 구분됩니다.

Header.Payload.Signature

JWT의 구성 요소

  • Header (헤더): 토큰의 타입(typ)과 서명에 사용된 알고리즘(alg) 정보를 포함합니다. 일반적으로 Base64Url로 인코딩되어 있습니다.
    {
      "alg": "HS256",
      "typ": "JWT"
    }
  • Payload (페이로드): 토큰에 담을 정보, 즉 클레임(Claim)들을 포함합니다. 클레임은 사용자 ID, 권한, 토큰 만료 시간 등과 같은 데이터를 담을 수 있습니다. 이는 Base64Url로 인코딩되지만, 암호화된 것이 아니므로 민감한 정보를 직접 넣어서는 안 됩니다.
    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022,
      "exp": 1516242622
    }
  • Signature (서명): 인코딩된 헤더와 페이로드, 그리고 서버의 비밀 키(Secret Key)를 사용하여 생성됩니다. 이 서명은 토큰의 무결성을 검증하는 데 사용됩니다. 즉, 토큰 내용이 중간에 변조되지 않았음을 보장합니다.
    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret_key
    )

JWT 기반 인증 흐름

일반적인 JWT 인증 흐름은 다음과 같습니다.

  1. 사용자가 아이디와 비밀번호를 입력하여 로그인 요청을 보냅니다.
  2. 서버는 사용자 정보를 검증하고, 유효한 사용자라면 JWT(Access Token)를 발급하여 클라이언트에게 응답합니다.
  3. 클라이언트는 발급받은 Access Token을 안전하게 저장합니다 (예: 웹의 경우 로컬 스토리지, 세션 스토리지, 쿠키 등).
  4. 이후 클라이언트가 서버에 보호된 리소스(API)를 요청할 때마다 HTTP 헤더의 Authorization 필드에 Access Token을 첨부하여 보냅니다.
  5. 서버는 요청에 포함된 Access Token의 서명을 검증하고, 유효한 토큰이라면 Payload의 정보를 바탕으로 사용자를 인증하고 권한을 확인한 후 요청을 처리합니다.

이 과정에서 서버는 사용자 세션 정보를 직접 저장하지 않으므로, 서버 확장성이 크게 향상됩니다.

일반적인 JWT 인증 시스템 설계 패턴

JWT의 무상태성은 큰 장점이지만, Access Token이 탈취될 경우 이를 즉시 무효화하기 어렵다는 단점을 내포합니다. 이를 보완하고 보안성을 높이기 위해 Access Token과 Refresh Token을 분리하여 사용하는 패턴이 널리 사용됩니다.

Access Token과 Refresh Token의 역할

  • Access Token (접근 토큰):
    • 목적: 실제 리소스 접근에 사용되는 토큰.
    • 유효 기간: 짧게 설정 (예: 10분 ~ 30분). 탈취 위험을 최소화하기 위함입니다.
    • 저장 위치: 주로 클라이언트 메모리나 웹 스토리지(Local Storage, Session Storage)에 저장하여 API 요청 시 HTTP 헤더에 담아 보냅니다. 보안이 강화된 방식으로는 HttpOnly 쿠키 사용도 고려됩니다.
  • Refresh Token (갱신 토큰):
    • 목적: Access Token이 만료되었을 때 새로운 Access Token을 발급받기 위한 토큰.
    • 유효 기간: 길게 설정 (예: 1일 ~ 14일, 혹은 그 이상).
    • 저장 위치: Access Token보다 훨씬 민감하므로 HttpOnly, Secure, SameSite 속성이 설정된 쿠키에 저장하는 것이 일반적입니다. 이는 JavaScript 접근을 차단하고, HTTPS 통신에서만 전송되도록 하며, CSRF 공격을 방어하는 데 도움을 줍니다.
    • 관리: 서버 측에서 Refresh Token의 유효성을 관리하고, 필요시 폐기할 수 있도록 데이터베이스에 저장하는 경우가 많습니다.

인증 흐름 (Access Token + Refresh Token)

  1. 사용자가 로그인하면, 서버는 짧은 수명의 Access Token긴 수명의 Refresh Token을 모두 발급합니다.
  2. Access Token은 클라이언트 메모리나 웹 스토리지에, Refresh Token은 HttpOnly 쿠키에 저장됩니다.
  3. 클라이언트는 Access Token을 사용하여 API 요청을 보냅니다.
  4. Access Token이 만료되면, 클라이언트는 Refresh Token을 사용하여 Access Token 재발급 요청을 보냅니다.
  5. 서버는 Refresh Token의 유효성을 검증하고, 새로운 Access Token을 발급하여 클라이언트에게 보냅니다. 이때 Refresh Token 자체도 재발급하여 토큰 재사용 공격(Refresh Token Rotation)을 방어할 수 있습니다.
  6. 사용자가 로그아웃하면, 서버는 Refresh Token을 무효화(블랙리스트에 추가하거나 DB에서 삭제)하여 재사용을 방지합니다.

이러한 패턴은 Access Token이 탈취되더라도 짧은 시간 내에 만료되어 피해를 최소화하고, Refresh Token은 비교적 안전한 방식으로 관리하여 사용자 경험을 유지하는 데 기여합니다.

JWT(JSON Web Token) 기반 인증 시스템 설계 및 구현 가이드: 보안 취약점 분석 및 방어 - computer, security, company, secure id, token, security, token, token, token, token, token

Image by Lalmch on Pixabay

JWT 기반 인증 시스템의 주요 보안 취약점 분석

JWT는 편리하지만, 잘못 설계하거나 구현하면 심각한 보안 문제를 야기할 수 있습니다. 주요 취약점들을 살펴보겠습니다.

토큰 탈취 (Token Theft)

가장 흔하고 치명적인 취약점 중 하나는 Access Token이나 Refresh Token이 공격자에게 탈취되는 경우입니다. 탈취된 토큰은 공격자가 마치 정당한 사용자인 것처럼 시스템에 접근할 수 있도록 허용합니다.

  • XSS (Cross-Site Scripting): 악성 스크립트가 웹 페이지에 주입되어 사용자 브라우저에서 실행될 때, 로컬 스토리지에 저장된 Access Token을 탈취할 수 있습니다.
  • CSRF (Cross-Site Request Forgery): 공격자가 사용자의 세션 권한을 도용하여 원치 않는 요청을 보내게 만드는 공격입니다. 쿠키에 저장된 토큰이 CSRF에 취약할 수 있습니다.
  • MITM (Man-in-the-Middle): HTTPS가 적용되지 않은 환경에서 통신을 가로채 토큰을 탈취할 수 있습니다.

서명 알고리즘 조작 (Signature Algorithm Manipulation)

JWT의 서명은 토큰의 무결성을 보장하는 핵심 요소입니다. 이 서명 알고리즘과 관련하여 여러 취약점이 발생할 수 있습니다.

  • 'None' 알고리즘 취약점: JWT 헤더의 "alg" 필드를 "none"으로 설정하면, 서버는 토큰 서명 검증을 건너뛰게 됩니다. 공격자는 이 취약점을 이용해 임의의 페이로드로 토큰을 생성하고, 서명 없이도 유효한 토큰인 것처럼 위장하여 서버를 속일 수 있습니다.
  • 약한 서명 키 (Weak Secret Key): 서버에서 사용하는 비밀 키가 너무 짧거나 예측하기 쉬울 경우, 공격자는 무작위 대입 공격(Brute-force Attack)이나 사전 공격을 통해 비밀 키를 알아내고, 유효한 서명을 위조하여 임의의 토큰을 생성할 수 있습니다.

JWT 무효화 및 만료 관리의 어려움

JWT의 Stateless 특성은 장점인 동시에 단점이 됩니다. 한 번 발급된 Access Token은 만료될 때까지 유효하며, 서버에서 강제로 무효화하기 어렵습니다.

  • 강제 로그아웃/권한 변경 미반영: 관리자가 특정 사용자를 강제 로그아웃시키거나 권한을 변경하더라도, 이미 발급된 Access Token은 만료 시간까지 계속 유효하므로 즉시 반영되지 않습니다.
  • 탈취된 Access Token 무효화 불가: Access Token이 탈취되었을 때, 서버는 해당 토큰을 즉시 무효화할 수 있는 직접적인 메커니즘이 없습니다. 만료 시간까지 공격자는 탈취된 토큰을 계속 사용할 수 있습니다.

민감 정보 포함 및 과도한 페이로드

JWT Payload는 Base64Url로 인코딩될 뿐, 암호화되지 않습니다. 이는 누구나 쉽게 디코딩하여 내용을 확인할 수 있다는 의미입니다.

  • 민감 정보 노출: 사용자 비밀번호, 주민등록번호 등 민감한 개인 식별 정보를 Payload에 포함하면, 토큰이 탈취될 경우 해당 정보가 그대로 노출됩니다.
  • 과도한 페이로드 크기: Payload에 너무 많은 정보를 담으면 토큰 크기가 커져 네트워크 트래픽 증가 및 성능 저하를 초래할 수 있습니다.

각 취약점에 대한 실용적인 방어 전략

위에서 언급된 취약점들을 방어하기 위한 구체적이고 실용적인 방법들을 살펴보겠습니다.

토큰 탈취 방어

  • HTTPS 강제 적용: 모든 통신은 반드시 HTTPS를 사용해야 합니다. 이는 MITM 공격으로부터 토큰을 보호하는 가장 기본적인 방어선입니다.
  • Access Token 저장 위치 신중 선택:
    • Local Storage/Session Storage: 개발 편의성이 높지만, XSS 공격에 매우 취약합니다. 악성 스크립트가 실행되면 쉽게 토큰을 탈취할 수 있습니다.
    • HttpOnly Cookie: Access Token을 HttpOnly 쿠키에 저장하면 JavaScript를 통한 접근이 차단되므로 XSS 공격으로부터 비교적 안전합니다. 하지만 CSRF 공격에는 취약할 수 있으므로, CSRF 토큰을 함께 사용하거나 SameSite 속성을 Strict 또는 Lax로 설정해야 합니다.
    • 메모리(Memory): 가장 안전한 방법 중 하나로, SPA(Single Page Application)에서 Access Token을 브라우저 메모리에만 저장하고, 페이지 새로고침 시 Refresh Token을 통해 재발급받는 방식입니다. XSS 및 CSRF 공격에 모두 강하지만, UX 측면에서 불편함이 있을 수 있습니다.
  • Refresh Token은 HttpOnly, Secure, SameSite 쿠키에 저장: Refresh Token은 Access Token보다 유효 기간이 길기 때문에 더욱 철저하게 보호해야 합니다.
    • HttpOnly: JavaScript 접근 차단.
    • Secure: HTTPS 연결에서만 전송.
    • SameSite=Strict (또는 Lax): CSRF 공격 방어에 효과적.

서명 알고리즘 및 키 관리 강화

  • 'None' 알고리즘 사용 금지: 서버는 JWT를 검증할 때 "alg": "none"으로 설정된 토큰을 절대로 허용해서는 안 됩니다. 사용하는 JWT 라이브러리가 기본적으로 이를 방어하는지 확인하고, 명시적으로 `none` 알고리즘을 거부하도록 설정해야 합니다.
  • 강력한 서명 키 사용 및 관리:
    • 길고 복잡한 비밀 키를 사용합니다 (최소 32바이트 이상). 무작위 대입 공격으로부터 안전하게 보호될 수 있도록 충분한 엔트로피를 가져야 합니다.
    • 비밀 키는 환경 변수, 키 관리 서비스(KMS), 또는 안전한 설정 파일에 저장하고, 코드에 하드코딩하지 않습니다.
    • 정기적인 키 로테이션: 비밀 키를 주기적으로 변경하여, 만약 키가 유출되더라도 피해를 최소화할 수 있도록 합니다.
  • 적절한 서명 알고리즘 선택:
    특징 HS256 (HMAC-SHA256) RS256 (RSA-SHA256)
    알고리즘 타입 대칭 키(Symmetric Key) 비대칭 키(Asymmetric Key)
    키 종류 하나의 비밀 키 (서명/검증 모두 사용) 개인 키(서명)와 공개 키(검증)
    보안성 비밀 키가 노출되면 취약 개인 키만 안전하면 공개 키 노출 무관
    주요 사용처 단일 서비스, 마이크로서비스 간 내부 통신 여러 서비스에 걸친 인증, 서드파티 통합 (Google, Facebook 등)
    복잡성 상대적으로 간단 키 관리(생성, 배포) 복잡
    일반적인 서비스에서는 HS256도 충분하지만, 여러 서비스가 토큰을 발행하고 검증하거나, 서드파티 서비스와 연동할 때는 RS256과 같은 비대칭 키 알고리즘이 더 적합합니다.

JWT 무효화 및 만료 관리 전략

무상태 JWT의 단점을 보완하기 위해 다음과 같은 전략을 사용합니다.

  • Access Token 짧은 유효 기간 설정: Access Token의 유효 기간을 짧게 설정(예: 10~30분)하여, 탈취되더라도 공격자가 사용할 수 있는 시간을 최소화합니다.
  • Refresh Token 관리 및 재사용 감지:
    • Refresh Token은 서버 DB에 저장하고, 사용자가 로그아웃하거나 관리자가 강제 로그아웃 시킬 경우 DB에서 해당 Refresh Token을 삭제하여 무효화합니다.
    • Refresh Token Rotation: Refresh Token이 사용될 때마다 새로운 Refresh Token을 발급하고 기존 Refresh Token을 무효화하는 방식입니다. 만약 탈취된 Refresh Token이 재사용되면, 이를 감지하고 모든 토큰을 무효화하여 공격자의 접근을 차단할 수 있습니다.
    • 블랙리스트(Blacklist): 탈취되었거나 강제로 무효화해야 하는 Access Token들을 서버 측 블랙리스트(예: Redis 같은 인메모리 DB)에 저장하여, 이후 요청 시 해당 토큰이 블랙리스트에 있는지 확인하고 접근을 거부합니다. 짧은 유효 기간의 Access Token에는 오버헤드가 발생할 수 있으므로, Refresh Token 관리가 더 중요합니다.
  • 토큰 만료 시간(exp) 및 발행 시간(iat) 클레임 활용: 모든 JWT에 exp(expiration time) 클레임을 필수로 포함하여 토큰의 유효 기간을 명시하고, 서버는 이 시간을 기준으로 토큰의 유효성을 검증해야 합니다. iat(issued at) 클레임도 함께 사용하여 토큰의 생성 시점을 확인할 수 있습니다.

페이로드 최소화 및 암호화 고려

  • 민감 정보는 페이로드에 포함 금지: JWT Payload에는 절대로 비밀번호, 개인 식별 정보(PII) 등 민감한 데이터를 포함해서는 안 됩니다. 대신 사용자 ID와 같은 최소한의 식별 정보만 포함하고, 필요한 상세 정보는 서버에서 해당 ID를 사용하여 조회해야 합니다.
  • JWE (JSON Web Encryption) 고려: 만약 Payload에 반드시 민감한 정보를 포함해야 하는 상황이라면, JWT의 암호화 버전인 JWE(JSON Web Encryption)를 고려할 수 있습니다. JWE는 토큰 자체를 암호화하여 Payload 내용을 보호하지만, 구현의 복잡성이 증가하므로 신중하게 결정해야 합니다.
  • 페이로드 크기 최적화: Payload에 불필요한 정보를 최소화하여 토큰 크기를 줄이고, 네트워크 전송 효율성을 높입니다.
JWT(JSON Web Token) 기반 인증 시스템 설계 및 구현 가이드: 보안 취약점 분석 및 방어 - man, face, facial recognition, biometric, identify, security, people, authentication, identification, database, scanning, facial recognition, facial recognition, facial recognition, facial recognition, facial recognition, biometric

Image by Tumisu on Pixabay

JWT 구현 시 추가 고려사항 및 모범 사례

위에서 제시된 방어 전략 외에도, JWT 기반 인증 시스템을 더욱 견고하게 만들기 위한 몇 가지 추가적인 고려사항들이 있습니다.

  • CORS (Cross-Origin Resource Sharing) 설정: 클라이언트와 서버가 다른 도메인에 있을 경우, CORS 설정을 올바르게 구성해야 합니다. 특히 인증 정보(Credentials)를 포함하는 요청을 허용할지, 특정 헤더를 허용할지 등을 세심하게 설정해야 합니다.
  • Rate Limiting (요청 제한): 인증 엔드포인트(로그인, 토큰 재발급 등)에 Rate Limiting을 적용하여 무차별 대입 공격이나 남용을 방지합니다. 특정 IP 주소나 사용자로부터의 과도한 요청을 제한하여 시스템 부하를 줄이고 보안을 강화합니다.
  • 로깅 및 모니터링: 인증 실패, 토큰 검증 실패, 비정상적인 토큰 재발급 시도 등 보안 관련 이벤트들을 상세히 로깅하고, 이상 징후를 실시간으로 모니터링하여 잠재적인 공격을 조기에 감지할 수 있도록 합니다.
  • 오픈소스 라이브러리 활용: JWT 구현은 직접 작성하기보다 잘 검증된 오픈소스 라이브러리(예: Java의 JJWT, JavaScript의 jsonwebtoken, Python의 PyJWT 등)를 사용하는 것이 안전합니다. 이러한 라이브러리들은 다양한 보안 취약점에 대한 방어 메커니즘을 내장하고 있습니다.
  • 클라이언트 측 보안 강화: 클라이언트 애플리케이션(특히 SPA)에서 XSS 취약점을 최소화하기 위해 콘텐츠 보안 정책(CSP)을 엄격하게 적용하고, 입력값 검증을 철저히 하는 등 기본적인 웹 보안 수칙을 준수해야 합니다.
// 예시: Node.js (jsonwebtoken 라이브러리)에서 JWT 서명 및 검증 시 'none' 알고리즘 방어
const jwt = require('jsonwebtoken');

const secretKey = process.env.JWT_SECRET_KEY; // 환경 변수에서 강력한 키 로드

// JWT 발행 (HS256 사용)
const token = jwt.sign({ userId: 'user123', role: 'admin' }, secretKey, {
  algorithm: 'HS256', // 명시적으로 HS256 사용
  expiresIn: '15m'
});
console.log('Generated JWT:', token);

// JWT 검증
try {
  const decoded = jwt.verify(token, secretKey, {
    algorithms: ['HS256'] // 허용할 알고리즘 명시. 'none' 포함X
  });
  console.log('Decoded JWT:', decoded);
} catch (error) {
  if (error instanceof jwt.TokenExpiredError) {
    console.error('Token Expired:', error.message);
  } else if (error instanceof jwt.JsonWebTokenError) {
    console.error('Invalid Token:', error.message);
    // 'none' 알고리즘 공격 시에도 JsonWebTokenError 발생 (invalid signature)
  } else {
    console.error('Verification Error:', error.message);
  }
}

// 'none' 알고리즘 공격 시도 (예시: 실제 코드에서는 이런 토큰을 생성하지 말 것)
// const maliciousToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VySWQiOiJhZHJvbjEiLCJyb2xlIjoiYWRtaW4ifQ.";
// try {
//   const decodedMalicious = jwt.verify(maliciousToken, secretKey, {
//     algorithms: ['HS256'] // 'none'을 허용하지 않음
//   });
//   console.log('Decoded malicious JWT:', decodedMalicious);
// } catch (error) {
//   console.error("Malicious token verification failed:", error.message); // 예상되는 에러
// }

위 코드 예시에서 jwt.verify 함수의 algorithms 옵션에 ['HS256']과 같이 허용할 알고리즘을 명시적으로 지정하는 것이 중요합니다. 이렇게 하면 "alg": "none"과 같은 공격 시도를 효과적으로 방어할 수 있습니다.

JWT 보안, 이제는 자신 있게! (결론)

JWT는 현대 웹 및 모바일 애플리케이션의 확장성유연성을 크게 향상시키는 강력한 인증 메커니즘입니다. 하지만 그 편리함에만 집중하여 보안 측면을 간과한다면, 시스템에 치명적인 약점을 노출할 수 있습니다. 이 글에서 다룬 토큰 탈취, 서명 알고리즘 조작, 무효화 관리의 어려움, 민감 정보 노출 등의 주요 취약점들을 명확히 이해하고, 이에 대한 실용적인 방어 전략들을 철저히 적용하는 것이 중요합니다.

안전한 JWT 기반 인증 시스템을 구축하기 위해서는 설계 단계부터 보안을 최우선으로 고려해야 합니다. HTTPS 적용, 강력한 서명 키 사용 및 관리, Access Token과 Refresh Token의 적절한 분리 및 저장, Refresh Token Rotation, 그리고 'none' 알고리즘 방어와 같은 모범 사례들을 반드시 준수해야 합니다. 잘 설계되고 구현된 JWT 인증 시스템은 사용자에게 안전하고 쾌적한 경험을 제공할 것입니다.

JWT 구현 시 겪었던 어려움이나 특별한 보안 팁이 있다면, 아래 댓글로 공유하여 함께 성장하는 기회가 되었으면 합니다!

📌 함께 읽으면 좋은 글

  • [보안] DevSecOps를 위한 SAST, DAST, SCA 도입 전략: CI/CD 파이프라인 보안 테스트 자동화
  • [개발 도구] Postman 활용 가이드: API 개발 및 테스트 효율을 극대화하는 방법
  • [보안] 시크릿 관리 자동화: 개발부터 프로덕션까지 민감 정보 처리 전략

이 글이 도움이 되셨다면 공감(♥)댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.

반응형