튜토리얼

JWT 인증 구현 단계별 가이드

강코의 코딩 일기 2026. 3. 12. 12:01

JWT authentication, JWT implementation, Token-based security

안녕하세요! 오늘날 웹 애플리케이션 개발에서 사용자 인증은 필수적인 요소입니다. 특히 확장성보안성을 동시에 고려해야 하는 현대 분산 시스템에서는 더욱 그렇습니다. 기존의 세션 기반 인증 방식의 한계를 극복하고 등장한 강력한 대안 중 하나가 바로 JWT(JSON Web Token) 인증입니다.

이 글에서는 JWT가 무엇인지부터 시작하여, 실제 시스템에 JWT 인증을 어떻게 구현할 수 있는지 단계별로 자세히 알아보겠습니다. JWT 인증 도입을 고민하고 계시거나, 이미 알고 있지만 더 깊이 있는 구현 가이드가 필요하셨던 분들에게 실질적인 도움이 되기를 바랍니다.

JWT 인증 구현 단계별 가이드

Image by Boskampi on Pixabay

JWT란 무엇인가?

JWT(JSON Web Token)는 웹 표준(RFC 7519)으로, 당사자 간에 정보를 안전하게 전송하기 위한 간결하고 자체 포함적인(Self-contained) 방법입니다. JWT는 주로 인증 및 정보 교환에 사용되며, 디지털 서명되어 있어 정보의 무결성신뢰성을 보장합니다.

자체 포함적(Self-contained)이란, 토큰 자체에 필요한 모든 사용자 정보와 메타데이터가 포함되어 있어, 별도의 서버 저장소(예: 세션 저장소) 없이도 토큰만으로 인증에 필요한 정보를 얻을 수 있다는 의미입니다.

JWT는 일반적으로 세 부분으로 구성됩니다:

  • Header (헤더): 토큰의 타입(JWT)과 서명에 사용된 알고리즘(예: HMAC SHA256 또는 RSA)을 정의합니다.
    {
      "alg": "HS256",
      "typ": "JWT"
    }
  • Payload (페이로드): 토큰에 담을 정보(클레임)를 포함합니다. 클레임은 사용자 ID, 권한, 토큰 만료 시간 등과 같은 엔티티에 대한 속성입니다.
    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022,
      "exp": 1516242622,
      "admin": true
    }
  • Signature (서명): 인코딩된 헤더, 인코딩된 페이로드, 그리고 비밀 키(Secret Key)를 사용하여 생성됩니다. 이 서명은 토큰이 변조되지 않았음을 확인하고, 토큰이 유효한 발급자로부터 왔음을 검증하는 데 사용됩니다.

이 세 부분은 각각 Base64Url로 인코딩된 후 점(.)으로 연결되어 하나의 JWT 문자열을 형성합니다: AAAA.BBBB.CCCC

JWT 인증 플로우 이해

JWT를 이용한 인증 과정은 다음과 같은 단계로 이루어집니다:

  1. 사용자 로그인: 클라이언트(웹 브라우저, 모바일 앱 등)가 사용자 ID와 비밀번호를 서버로 전송하여 로그인을 시도합니다.
  2. 토큰 발급: 서버는 전송받은 사용자 정보를 검증하고, 인증에 성공하면 해당 사용자에 대한 JWT(Access Token)를 생성합니다. 이때, 토큰의 Payload에는 사용자 식별 정보(예: `userId`)와 같은 필요한 데이터를 포함시킵니다. 경우에 따라 Refresh Token도 함께 발급될 수 있습니다.
  3. 토큰 전달: 서버는 생성된 JWT를 클라이언트에 응답으로 보냅니다. 클라이언트는 이 토큰을 안전하게 저장합니다(예: Local Storage, HttpOnly Cookie).
  4. 리소스 요청: 클라이언트가 보호된 리소스(API)에 접근하려고 할 때마다, 저장된 JWT를 요청 헤더의 `Authorization` 필드(일반적으로 `Bearer` 스키마와 함께)에 담아 서버로 전송합니다.
    Authorization: Bearer [YOUR_JWT_TOKEN]
  5. 토큰 검증: 서버는 클라이언트로부터 받은 JWT를 검증합니다.
    • 토큰의 서명이 유효한지 확인합니다. (비밀 키로 서명 재검증)
    • 토큰이 만료되지 않았는지 확인합니다.
    • 필요한 경우, 토큰의 Payload에 포함된 사용자 정보를 추출하여 권한을 확인합니다.
  6. 응답 반환: 토큰 검증에 성공하면 서버는 요청된 리소스에 대한 접근을 허용하고 응답을 반환합니다. 검증에 실패하면, 접근 거부(Unauthorized) 응답을 보냅니다.

세션 기반 인증 vs. JWT 인증

JWT 인증은 기존의 세션 기반 인증 방식과 여러 면에서 차이를 보입니다. 주요 특징을 비교 테이블로 살펴보겠습니다.

특징 세션 기반 인증 JWT 인증
상태 관리 서버가 세션 ID와 사용자 정보를 저장 (Stateful) 서버가 사용자 정보를 저장하지 않음 (Stateless)
확장성 세션 공유를 위한 추가적인 인프라 필요 (DB, Redis 등) 각 서버가 독립적으로 토큰 검증 가능, 분산 환경에 유리
보안 CSRF 공격에 취약할 수 있음, 세션 하이재킹 위험 XSS 공격에 취약할 수 있음, 토큰 탈취 시 위험
오버헤드 세션 저장 및 조회에 따른 서버 부하 발생 토큰 크기 증가 시 네트워크 오버헤드, 서명 검증 연산 필요
사용처 모놀리식 애플리케이션, 단일 서버 환경 마이크로서비스, 모바일 앱, SPA(Single Page Application)
JWT 인증 구현 단계별 가이드

Image by Innovalabs on Pixabay

JWT 구현을 위한 준비물

JWT 인증을 구현하기 위해서는 다음과 같은 사항들을 준비해야 합니다:

  • JWT 라이브러리: 사용하는 프로그래밍 언어 및 프레임워크에 맞는 JWT 라이브러리가 필요합니다.
    • Node.js: jsonwebtoken
    • Java: jjwt, Auth0 Java JWT
    • Python: PyJWT
    • Go: github.com/golang-jwt/jwt
    • ...등 다양한 언어별 라이브러리가 존재합니다.
  • 비밀 키(Secret Key): 토큰을 서명하고 검증할 때 사용되는 매우 중요한 키입니다. 이 키는 절대로 외부에 노출되어서는 안 됩니다. 보안을 위해 환경 변수나 보안 저장소에 보관해야 합니다. 길고 복잡한 무작위 문자열을 사용하는 것이 좋습니다.
  • 사용자 저장소: 사용자 정보를 저장하고 인증 시 검증할 데이터베이스(RDBMS, NoSQL 등) 또는 외부 인증 서비스가 필요합니다.
  • (선택 사항) Refresh Token 저장소: Refresh Token을 사용하는 경우, 이 토큰들을 안전하게 저장하고 관리할 데이터베이스 또는 캐시(예: Redis)가 필요합니다.
JWT 인증 구현 단계별 가이드

Image by StockSnap on Pixabay

JWT 구현 단계별 가이드

이제 실제 코드 레벨에서의 구현 단계를 살펴보겠습니다. 아래 예시는 특정 언어나 프레임워크에 종속되지 않는 개념적인 코드입니다.

1. 토큰 발행 (Issuing a Token)

사용자가 로그인 요청을 하면, 서버는 사용자 정보를 확인하고 유효한 경우 JWT를 발행합니다.

// 1. 사용자 로그인 요청 처리
function login(username, password) {
  const user = authenticateUser(username, password); // 사용자 인증 로직
  if (!user) {
    throw new Error("Invalid credentials");
  }

  // 2. Payload 생성 (사용자 식별 정보, 권한, 만료 시간 등)
  const payload = {
    userId: user.id,
    roles: user.roles,
    // iss: 'your-app-name', // 발급자 (Issuer)
    // aud: 'your-client-id' // 수신자 (Audience)
  };

  // 3. 토큰 옵션 설정 (만료 시간 등)
  const options = {
    expiresIn: '1h' // Access Token은 짧게 (예: 1시간)
  };

  // 4. 비밀 키를 사용하여 토큰 서명 및 발행
  const accessToken = jwt.sign(payload, SECRET_KEY, options);

  // (선택 사항) Refresh Token 발행
  const refreshToken = jwt.sign({ userId: user.id }, REFRESH_SECRET_KEY, { expiresIn: '7d' });
  saveRefreshToken(user.id, refreshToken); // Refresh Token 저장

  return { accessToken, refreshToken };
}

2. 토큰 전달 (Token Transmission)

발행된 토큰은 클라이언트에 전달되어야 합니다. 일반적으로 HTTP 응답 바디에 JSON 형태로 포함시켜 클라이언트로 보냅니다.

// 서버 응답 예시
HTTP/1.1 200 OK
Content-Type: application/json

{
  "message": "Login successful",
  "accessToken": "eyJhbGciOiJIUzI1Ni...",
  "refreshToken": "eyJhbGciOiJIUzI1Ni..."
}

클라이언트는 이 토큰을 받아 Local Storage, Session Storage 또는 HttpOnly Cookie에 저장합니다. 이후 보호된 리소스 요청 시 `Authorization` 헤더에 `Bearer` 스키마와 함께 Access Token을 포함하여 전송합니다.

// 클라이언트 요청 예시
GET /api/protected-resource HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1Ni...

3. 토큰 검증 (Token Verification)

서버는 보호된 리소스에 대한 요청을 받을 때마다, 요청 헤더에서 JWT를 추출하고 그 유효성을 검증합니다.

// 미들웨어 또는 인터셉터에서 토큰 검증
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN" 에서 TOKEN 추출

  if (token == null) return res.sendStatus(401); // 토큰 없음 (Unauthorized)

  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ message: 'Token expired' }); // 토큰 만료
      }
      return res.sendStatus(403); // 유효하지 않은 토큰 (Forbidden)
    }
    req.user = user; // 검증된 사용자 정보를 요청 객체에 추가
    next(); // 다음 미들웨어 또는 라우트 핸들러로 진행
  });
}

// 보호된 라우트에 미들웨어 적용
// app.get('/api/protected-resource', authenticateToken, (req, res) => {
//   res.json({ message: `Welcome, ${req.user.userId}! This is protected data.` });
// });

4. 토큰 갱신 (Token Refresh)

Access Token의 만료 시간이 짧기 때문에, 사용자 경험을 위해 Refresh Token을 사용하여 새로운 Access Token을 발급받는 메커니즘이 필요합니다.

// Refresh Token을 이용한 Access Token 갱신
function refreshAccessToken(refreshToken) {
  // 1. Refresh Token 검증
  jwt.verify(refreshToken, REFRESH_SECRET_KEY, (err, user) => {
    if (err) throw new Error("Invalid refresh token");

    // 2. 저장된 Refresh Token과 일치하는지 확인 (DB 조회)
    const storedRefreshToken = getStoredRefreshToken(user.userId);
    if (!storedRefreshToken || storedRefreshToken !== refreshToken) {
      throw new Error("Refresh token not found or invalid");
    }

    // 3. 새로운 Access Token 발행
    const newAccessToken = jwt.sign(
      { userId: user.userId, roles: user.roles }, // 사용자 정보는 DB에서 다시 조회하는 것이 안전
      SECRET_KEY,
      { expiresIn: '1h' }
    );

    return newAccessToken;
  });
}

JWT 보안 고려사항

JWT는 강력한 인증 방식이지만, 몇 가지 보안 취약점을 인지하고 적절히 대응해야 합니다.

  • 비밀 키(Secret Key) 관리: 가장 중요합니다. 비밀 키는 강력하고 예측 불가능해야 하며, 외부에 노출되지 않도록 환경 변수나 보안 관리 시스템을 통해 안전하게 관리되어야 합니다. 주기적인 키 변경(Key Rotation)도 고려할 수 있습니다.
  • HTTPS 사용: 토큰이 네트워크를 통해 전송될 때 가로채는 것을 방지하기 위해 반드시 HTTPS를 사용해야 합니다.
  • 토큰 만료 시간 설정: Access Token은 짧은 만료 시간을 가지도록 설정하여, 토큰이 탈취되더라도 공격자가 사용할 수 있는 시간을 최소화해야 합니다. Refresh Token은 더 긴 만료 시간을 가질 수 있지만, 이를 안전하게 관리해야 합니다.
  • 토큰 탈취 방지 (XSS/CSRF):
    • XSS(Cross-Site Scripting): 클라이언트 측 JavaScript를 통해 Local Storage에 저장된 JWT가 탈취될 수 있습니다. 이를 방지하기 위해 `HttpOnly` 속성을 가진 쿠키에 JWT를 저장하는 방법도 고려할 수 있습니다. (단, 이 경우 CSRF에 대한 추가적인 방어가 필요합니다.)
    • CSRF(Cross-Site Request Forgery): `HttpOnly` 쿠키를 사용하는 경우 CSRF 공격에 취약해질 수 있습니다. CSRF 토큰(SameSite Cookie, Referer 검사 등)을 함께 사용하여 방어해야 합니다.
  • 토큰 무효화(Revocation): JWT는 기본적으로 Stateless하기 때문에 서버에서 특정 토큰을 즉시 무효화하기 어렵습니다. 만료 기간이 짧은 Access Token과 더불어, Refresh Token을 데이터베이스에 저장하고 사용자가 로그아웃하거나 관리자가 강제로 로그아웃시킬 때 해당 Refresh Token을 무효화(삭제/블랙리스트)하는 방식으로 대응할 수 있습니다.

JWT는 분산 환경에서 매우 효과적인 인증 솔루션이지만, 그만큼 보안에 대한 깊은 이해와 신중한 구현이 필요합니다. 위 고려사항들을 충분히 숙지하고 적용하여 안전한 시스템을 구축하시길 바랍니다.

궁금한 점이나 보충하고 싶은 내용이 있다면 언제든지 댓글로 남겨주세요!