프론트엔드 로그인 방식: 세션, 토큰, 그리고 JWT

ClydeHan·2024년 12월 11일
29

jwt token logo image

본문을 이해하기 위한 간단한 사전 지식

1. 인증(Authentication)과 인가(Authorization)

  • 인증(Authentication)은 사용자가 주장하는 ID와 비밀번호가 맞는지 확인하는 과정이다. 주로 로그인 절차를 통해 수행된다.
  • 인가(Authorization)는 인증된 사용자가 특정 리소스에 접근할 권한이 있는지를 확인하는 절차이다. 권한 확인 과정이라고도 한다.

2. HTTP의 비상태성(Stateless)

  • HTTP는 요청 간의 상태를 저장하지 않는 Stateless 프로토콜이다.
  • 지속적인 인증이 필요한 경우, 세션(Session) 또는 토큰(Token) 기반 방식으로 인증 상태를 유지해야 한다.

세션 기반 인증과 토큰 기반 인증

1. 세션 기반 인증

개념

  • 서버가 인증된 사용자의 정보를 세션에 저장하고, 클라이언트는 세션 ID를 통해 서버에서 자신의 상태를 확인받는 방식이다.

작동 방식

  1. 사용자가 로그인 요청을 하면, 서버는 인증 후 세션을 생성하고 고유한 Session ID를 클라이언트에 발급한다.
  2. 클라이언트는 이 Session ID를 쿠키에 저장한다.
  3. 클라이언트는 이후 요청마다 쿠키에 포함된 Session ID를 서버로 전송한다.
  4. 서버는 세션 저장소에서 해당 ID를 참조해 인증 상태를 확인한다.

특징

  • 장점
    • 민감한 정보는 서버에서 관리하기 때문에 보안성이 높다.
    • 서버에서 세션을 삭제하면 즉시 로그아웃 처리할 수 있다.
    • 쿠키에 저장되는 데이터가 최소화된다(Session ID).
  • 단점
    • 서버가 세션 저장소를 유지해야 하므로 확장성이 낮다.
    • 세션 ID가 탈취되면 악용될 수 있다.
    • 상태 유지(Stateful) 방식이므로 서버 부담이 증가한다.

2. 토큰 기반 인증

개념

  • 사용자가 인증 후 서버에서 발급받은 토큰(JWT 등)을 이용하여 요청마다 인증을 수행하는 방식이다.

작동 방식

  1. 사용자가 로그인 요청을 하면, 서버는 인증 후 토큰(JWT)을 생성하여 클라이언트에 전달한다.
  2. 클라이언트는 토큰을 로컬 스토리지, 세션 스토리지 또는 쿠키에 저장한다.
  3. 요청마다 HTTP Authorization 헤더에 토큰을 포함하여 서버로 전송한다.
  4. 서버는 전달받은 토큰의 유효성을 검증하여 사용자 인증을 수행한다.

특징

  • 장점
    • Stateless 특성으로 서버는 인증 상태를 저장하지 않아 확장성이 뛰어나다.
    • 클라이언트가 인증 정보를 관리하므로 서버의 데이터베이스 조회가 줄어든다.
    • SPA, 모바일, REST API 등 다양한 플랫폼에서 적합하게 사용할 수 있다.
  • 단점
    • 클라이언트에 저장된 토큰이 탈취되면 방어하기 어렵다.
    • 토큰의 크기가 클 경우 네트워크 부하가 증가할 가능성이 있다.
    • 토큰의 Payload는 암호화되지 않기 때문에 민감한 정보를 포함해서는 안 된다.

3. 세션 기반 vs 토큰 기반 비교

항목토큰 기반 인증세션 기반 인증
서버 상태 유지 여부Stateless (서버 상태 저장 안 함)Stateful (서버 상태 저장)
인증 상태 저장 위치클라이언트(로컬/세션 스토리지, 쿠키)서버(세션 저장소: 메모리, 데이터베이스 등)
확장성높음: 서버 확장 용이. HTTP Stateless 활용 가능.낮음: 서버 확장이 복잡(Stiky Session, Clustering 등 필요).
보안 위험클라이언트가 정보 보관. 탈취 시 위험. (XSS, CSRF)인증 정보 서버 관리. 탈취 시 무효화 가능.
로그아웃 처리클라이언트가 토큰 삭제, 즉시 무효화 어려움서버에서 세션 삭제로 즉시 로그아웃 가능
성능클라이언트가 인증 데이터 관리. 서버 부담 없음.서버가 세션 데이터를 저장·관리. 부담 증가.
데이터 전송량큰 크기의 토큰 포함 (JWT 등)작은 크기의 세션 ID 전송
주요 사용 사례REST API, SPA, 모바일 앱 등전통적 웹 애플리케이션, 서버 렌더링 기반 사이트

토큰 기반 인증 방식: JWT (JSON Web Token)

1. JWT (JSON Web Token)

개념

  • JWT토큰 기반 인증에서 사용되는 대표적인 토큰 포맷이다.
  • JSON 데이터를 Base64 URL-safe 방식으로 인코딩하여 직렬화한 토큰이다.
  • 토큰 자체에 사용자 정보를 포함하고, 위변조 방지를 위해 서명(Signature)을 포함한다.
  • 주로 인증(Authentication)권한 부여(Authorization)에 사용되며, 서버가 클라이언트의 상태를 유지하지 않고도 클라이언트를 식별할 수 있도록 돕는다.

JWT의 구조

JWT는 세 부분으로 구성되며, 각 부분은 점(.)으로 구분된다. Header.Payload.Signature

  1. Header
    • 토큰의 유형(typ)과 서명에 사용된 알고리즘(alg) 정보를 포함한다.
    • 예시
      {
        "alg": "HS256",
        "typ": "JWT"
      }
  2. Payload
    • 사용자 정보와 클레임(Claim)을 포함한다.
    • 클레임의 유형
      • 등록된 클레임: 표준에서 정의된 예약 클레임 (예: iss, exp, sub 등).
      • 공개 클레임: 사용자 정의 클레임으로, 충돌 방지를 위해 네임스페이스를 사용하는 것이 좋다.
      • 비공개 클레임: 클라이언트와 서버 간의 협의로 정의된 클레임.
    • 예시
      {
        "sub": "1234567890",
        "name": "John Doe",
        "iat": 1516239022
      }
  3. Signature
    • Header와 Payload를 비밀 키를 사용해 암호화한 서명.
    • JWT의 위조를 방지하고 유효성을 검증하는 데 사용된다.
    • 생성 과정
      Signature = HMACSHA256(
        base64UrlEncode(header) + "." + base64UrlEncode(payload),
        secret
      )

JWT의 작동 과정

  1. JWT 생성
    • 사용자가 로그인하면 서버는 사용자의 정보를 바탕으로 JWT를 생성하여 클라이언트에 전달한다.
  2. JWT 전송
    • 클라이언트는 이후 요청 시 HTTP Authorization 헤더에 JWT를 포함해 서버로 전송한다.
      Authorization: Bearer <JWT>
  3. JWT 검증
    • 서버는 JWT의 서명을 검증하여 토큰의 유효성을 확인한다.
    • 유효한 경우 요청을 처리하며, 그렇지 않은 경우 오류를 반환한다.

JWT의 주요 특징

  • 무상태성 (Stateless): 서버는 클라이언트의 상태를 저장하지 않는다. 클라이언트가 JWT를 요청마다 전달하며, 이를 통해 인증이 이루어진다.
  • 토큰 기반 인증: JWT만으로 사용자를 인증할 수 있어, 추가적인 데이터베이스 조회가 필요 없다.
  • 확장성: 여러 서버에서 공유가 가능하며, 분산 시스템에서 유용하다.

장점

  1. 위변조 방지: 서명(Signature)을 통해 토큰의 위변조를 방지한다.
  2. Stateless: 서버가 세션 정보를 저장하지 않아 확장성이 뛰어나다.
  3. 유연성: SPA, 모바일 앱 등 다양한 클라이언트 환경에서 사용 가능하다.
  4. 사용의 편리성: 토큰은 자체적으로 인증 정보를 포함하므로, 인증이 간단하다.

단점

  1. 탈취 위험: 토큰이 탈취되면 만료 시간(expiry)이 지나기 전까지 악용될 수 있다.
  2. 비암호화 데이터: Payload는 암호화되지 않고 Base64로 인코딩되므로 민감한 정보를 포함해서는 안 된다.
  3. 크기 문제: 토큰의 크기가 커질 경우 네트워크 부하가 증가할 수 있다.

사용 예시

  • 로그인(인증) (fetch + JWT Token)
const BASE_URL = 'https://api.example.com';

async function login(username, password) {
    try {
        const response = await fetch(`${BASE_URL}/login`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ username, password }),
        });

        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(errorData.message || 'Login failed');
        }

        const data = await response.json();
        const token = data.token; // 서버에서 반환한 JWT 토큰
        localStorage.setItem('jwtToken', token); // 토큰 저장
        console.log('Login successful. Token stored.');
    } catch (error) {
        console.error('Login error:', error.message);
    }
}
  • API 호출(인가) (fetch + JWT Token)
async function fetchData(endpoint) {
    const token = localStorage.getItem('jwtToken'); // 로컬 스토리지에서 토큰 가져오기
    if (!token) {
        throw new Error('No token found. Please log in.');
    }

    try {
        const response = await fetch(`${BASE_URL}/${endpoint}`, {
            method: 'GET',
            headers: {
                'Authorization': `Bearer ${token}`, // Authorization 헤더 추가
            },
        });

        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(errorData.message || 'API call failed');
        }

        return await response.json();
    } catch (error) {
        console.error('API error:', error.message);
        throw error;
    }
}

2. Bearer Token

Bearer는 토큰 기반 인증에서 타입(Scheme)을 나타내는 표준화된 명칭이다. 이를 활용하여 클라이언트와 서버 간의 인증 요청을 일관성 있게 처리할 수 있다. Bearer를 사용하는 이유는 호환성과 표준화를 통해 인증 방식의 통일성을 확보하고, 다양한 환경에서 쉽게 적용할 수 있도록 하기 위함이다.

Bearer Token의 형식과 사용 방식

Bearer는 토큰 기반 인증에서 사용하는 타입(Scheme) 중 하나로, 서버가 요청을 인증할 때 사용하는 방식이다. Bearer Token은 HTTP 요청 헤더의 Authorization 필드에 포함되어 전송된다.

Authorization: <type> <credentials>
  • <type>은 Bearer가 되고, <credentials>는 실제 토큰 값이다.

  • Bearer Token은 JWT와 OAuth 같은 토큰을 HTTP 요청의 Authorization 헤더에 포함하여 전달하는 표준 인증 방식이다.

  • 서버와 클라이언트 간 통신

    • 클라이언트는 서버에서 발급받은 JWT를 요청 시 Authorization 헤더에 포함하여 전송한다.
    • 서버는 Authorization 헤더의 Bearer Token을 검증하여 인증 및 권한 부여를 수행한다.
  • Bearer Token은 "소유자(Bearer)가 해당 리소스에 접근할 권한이 있다"고 간주한다.


Bearer Token 등장 배경

초기에는 서비스마다 인증 방식이 달라, 타사 서비스나 공개 표준 프로토콜(OAuth 등)과의 호환이 어렵고, 사용자가 각 서비스에서 별도로 로그인해야 하는 불편함이 있었다. 이를 해결하기 위해 IETF는 OAuth 2.0과 함께 Bearer Token을 정의한 RFC 6750을 발표하였다. 이로써 Bearer Token은 JWT 또는 OAuth 토큰과 함께 사용하기에 적합한 인증 방식으로 자리잡았으며, 서비스 간의 일관된 인증 방식을 제공하기 위한 표준으로 널리 사용되고 있다.

IETF(Internet Engineering Task Force): 인터넷 표준을 개발하고 유지 관리하는 국제 조직

RFC(Request for Comments): 인터넷과 관련된 기술, 프로토콜, 시스템 등을 문서화한 공개 표준 또는 제안서


3. Access Token & Refresh Token

Access Token과 Refresh Token의 등장 배경 (JWT의 한계)

JWT를 단일 인증 수단으로 사용할 경우 발생할 수 있는 몇 가지 한계점이 있다.

  1. 유효 기간 관리: JWT는 Stateless한 특성으로 인해 발급 후 만료되기 전까지 강제로 철회(revoke)하기 어렵다.
  2. 보안 취약성: JWT가 탈취되면 만료 기간 동안 악용될 가능성이 있다.
  3. 사용자 경험 문제: JWT가 만료되면 사용자가 다시 로그인하거나 새로운 토큰을 발급받아야 하는 번거로움이 생긴다.
  4. 로그아웃 처리의 복잡성: Stateless한 특성으로 인해 세션 기반 로그아웃 같은 기능 구현이 까다로울 수 있다.

이러한 문제를 해결하기 위해 OAuth 2.0에서 Access Token과 Refresh Token 개념이 도입되었으며, JWT는 이 토큰을 구현하는 데 널리 사용된다. 즉, JWT가 Access/Refresh Token을 구현하는 포맷 중 하나로 자주 활용되는 것이다.


Access Token과 Refresh Token의 역할

  • Access Token
    • 개념
      • Access Token은 인증된 클라이언트가 리소스에 접근하기 위해 사용하는 짧은 유효 기간의 토큰이다.
      • 사용자가 로그인하면 서버는 Access Token을 발급하여 클라이언트에 전달한다.
    • 역할
      • 리소스 서버(API)에 요청을 보낼 때 인증을 수행하는 데 사용된다.
      • 짧은 유효 기간으로 설정되어 보안성을 강화한다.
    • 제한점
      • 유효 기간이 만료되면 새로 발급받아야 한다.
      • 탈취되면 유효 기간 동안 악용될 수 있다.
  • Refresh Token
    • 개념
      • Refresh Token은 새로운 Access Token을 발급받기 위해 사용하는 긴 유효 기간의 토큰이다.
      • 주로 데이터베이스와 같은 안전한 서버 저장소에 저장되며, 클라이언트에는 저장하지 않는 것이 일반적이다.
    • 역할
      • Access Token이 만료되었을 때, 클라이언트가 다시 인증 과정을 요구받지 않고 Refresh Token을 이용해 새로운 Access Token을 발급받는다.
      • 사용자 경험을 개선하며, Access Token 탈취로 인한 위험을 줄인다.
    • 제한점
      • Refresh Token 자체가 탈취되면 대처가 어렵다.

토큰 유출 문제와 대응 방안

Access Token과 Refresh Token은 사용자 인증 및 리소스 접근 관리에서 중요한 역할을 한다. 하지만 이 두 토큰이 유출될 경우 각각 고유의 보안 문제를 발생시킬 수 있다. 다음은 각 토큰의 유출 상황과 대응 방안이다.

  • Access Token 유출 Access Token이 탈취되었을 경우, 공격자는 유효 기간 동안 정상 사용자로 위장하여 서버 리소스에 접근할 수 있다. Access Token은 리소스에 접근하는 데 사용되므로, 민감한 데이터를 포함하거나 노출 위험이 높다. 공격자가 이를 악용하면 사용자의 권한을 무단으로 행사할 수 있다.
    • 대응 방안
      • 유효 기간을 짧게 설정:Access Token의 유효 기간을 15~30분으로 제한하여 탈취 시 사용할 수 있는 시간을 최소화한다. 이는 공격자의 활동 창구(attack window)를 줄이는 데 효과적이다.
      • 비정상적인 요청 탐지:토큰 사용 시 서버가 사용자 IP, User-Agent 등의 정보를 기록하여 이전 세션과 일치하지 않는 요청을 탐지하면 추가 인증을 요구하거나 토큰을 폐기한다. 이는 사용자 활동을 실시간으로 모니터링해 유출 시 즉각 대응할 수 있는 방법이다.
      • HTTPS 사용:모든 통신을 HTTPS로 암호화하여 네트워크 중간에서 토큰을 탈취하는 Man-in-the-Middle 공격을 방지한다.
      • Scope(권한) 최소화:Access Token의 사용 권한을 제한하여, 필요한 최소 범위(scope)로만 접근 가능하게 설정한다. 예를 들어, 읽기 전용 API만 호출할 수 있는 제한된 권한의 토큰을 발급하는 것이다.
      • 즉각적인 폐기 처리:사용자가 로그아웃하거나 의심스러운 활동이 탐지되면 해당 토큰을 서버와 클라이언트에서 즉시 폐기한다.
  • Refresh Token 유출 Refresh Token이 탈취되면, 공격자는 이를 사용해 새로운 Access Token을 지속적으로 발급받을 수 있다. 이는 Access Token의 유출보다 심각한 문제를 초래할 수 있다. 특히 Refresh Token은 긴 유효 기간을 가지기 때문에 장기간에 걸쳐 악용될 가능성이 높다.
    • 대응 방안
      • Refresh Token Rotation(RTR):Refresh Token을 한 번 사용할 때마다 새로운 토큰을 발급하고 이전 토큰을 즉시 폐기한다. 이를 통해 탈취된 토큰이 재사용되지 않도록 방지한다. 이는 스택오버플로우에서 가장 자주 권장되는 방법 중 하나이다.
      • 데이터베이스 검증:서버는 Refresh Token과 Access Token 쌍을 데이터베이스에 저장하여 요청 시 비교한다. 만약 불일치하거나 예상치 못한 활동이 감지되면 해당 토큰을 모두 폐기한다. 이 방식은 서버가 중앙에서 토큰 상태를 관리할 수 있게 해준다.
      • 비정상적 활동 탐지:Refresh Token 요청 시 사용자의 IP, User-Agent 정보를 확인하고 기존 세션과 다를 경우 토큰을 폐기한다.
      • 유효 기간 관리:Refresh Token의 유효 기간은 7~30일로 설정하여 장기적인 악용 가능성을 줄인다. Rotation 방식과 병행하면 더욱 안전하다.

토큰 저장 방식과 보안 고려 사항

토큰 저장 방식은 보안의 핵심이다. 특히 브라우저 환경에서는 XSS(Cross-Site Scripting)나 CSRF(Cross-Site Request Forgery) 공격에 대비하여 설계해야 한다.

  • Access Token 저장 Access Token은 리소스 접근 시 자주 사용되므로 저장 위치와 관리 방식이 중요하다.
    • 저장 위치 Access Token은 코드 내 메모리 변수에 저장하는 것이 가장 안전하다. 메모리는 브라우저 새로고침 시 삭제되므로 XSS로부터 보호된다. 로컬 스토리지나 세션 스토리지는 XSS 공격에 노출될 가능성이 있어 추천되지 않는다.
    • 보안 고려 사항
      1. XSS 방지:브라우저의 Content Security Policy(CSP)를 설정하여 악성 스크립트의 실행을 차단한다.
      2. HTTPS 필수:토큰이 전송되는 모든 요청은 HTTPS를 통해 암호화해야 한다.
      3. 재발급 전략:Refresh Token을 사용해 만료된 Access Token을 주기적으로 갱신하며, 사용자가 로그아웃하거나 의심스러운 활동이 감지되면 즉시 폐기한다.
  • Refresh Token 저장 Refresh Token은 긴 유효 기간을 가지며, Access Token 재발급에 사용되므로 더욱 안전한 저장 방식이 필요하다.
    • 저장 위치 Refresh Token은 HttpOnly 쿠키에 저장하는 것이 가장 안전하다. HttpOnly 속성은 자바스크립트로 접근할 수 없게 하여 XSS 공격에 대한 방어를 강화한다.
    • 보안 고려 사항
      1. CSRF 방지:Refresh Token 요청 시 CSRF 토큰을 함께 전송하여 요청의 출처를 검증한다. 쿠키의 SameSite 속성을 Strict 또는 Lax로 설정하여 의도치 않은 요청을 차단한다.
      2. Refresh Token Rotation:Refresh Token이 재사용되지 않도록, 사용 시마다 새로운 토큰을 발급하고 이전 토큰을 폐기한다.
      3. 유효 기간 관리:Refresh Token의 유효 기간은 길게 설정하되, 탈취 시 피해를 최소화하기 위해 Rotation 방식을 병행한다.
      4. IP 및 User-Agent 검증:Refresh Token 사용 시 기존 세션과 다른 IP나 디바이스에서 요청이 발생하면 토큰을 폐기한다.

Access Token과 Refresh Token의 실제 사용 코드 예시

  • Access Token 및 Refresh Token 발급 사용자가 로그인하면 서버에서 Access Token과 Refresh Token을 발급한다.
    async function login(username, password) {
        const response = await fetch('https://api.example.com/auth/login', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ username, password }),
        });
    
        if (!response.ok) {
            throw new Error('Login failed');
        }
    
        const data = await response.json();
        const { accessToken, refreshToken } = data;
    
        // Access Token은 메모리 또는 로컬 스토리지에 저장
        localStorage.setItem('accessToken', accessToken);
    
        // Refresh Token은 HttpOnly 쿠키로 전송되거나 로컬 스토리지에 저장
        localStorage.setItem('refreshToken', refreshToken);
    
        console.log('Login successful. Tokens stored.');
    }
    
  • API 호출 시 Access Token 사용 API 호출 시 Access Token을 Authorization 헤더에 포함하여 서버에 요청을 전송한다.
    async function fetchData(endpoint) {
        const accessToken = localStorage.getItem('accessToken');
        if (!accessToken) {
            throw new Error('No Access Token found. Please log in.');
        }
    
        const response = await fetch(`https://api.example.com/${endpoint}`, {
            method: 'GET',
            headers: {
                'Authorization': `Bearer ${accessToken}`,
            },
        });
    
        if (response.status === 401) {
            // Access Token 만료 시 처리
            const refreshed = await refreshAccessToken();
            if (refreshed) {
                return fetchData(endpoint); // 토큰 갱신 후 재요청
            } else {
                throw new Error('Session expired. Please log in again.');
            }
        }
    
        if (!response.ok) {
            throw new Error('API call failed');
        }
    
        return await response.json();
    }
    
  • Access Token 만료 시 Refresh Token 사용 Access Token이 만료된 경우, Refresh Token을 사용하여 새로운 Access Token을 발급받는다.
    async function refreshAccessToken() {
        const refreshToken = localStorage.getItem('refreshToken');
        if (!refreshToken) {
            return false; // Refresh Token 없음
        }
    
        const response = await fetch('https://api.example.com/auth/refresh', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ refreshToken }),
        });
    
        if (!response.ok) {
            return false; // 토큰 갱신 실패
        }
    
        const data = await response.json();
        const { accessToken } = data;
    
        // 새로운 Access Token 저장
        localStorage.setItem('accessToken', accessToken);
    
        return true; // 토큰 갱신 성공
    }

참고문헌

2개의 댓글

comment-user-thumbnail
2024년 12월 24일

정리글 잘 읽었습니다!

1개의 답글