소셜 로그인과 JWT를 활용한 안전한 인증 구현하기

oversleep·2025년 4월 24일

web-development

목록 보기
17/23
post-thumbnail

들어가며

웹 애플리케이션에서 소셜 로그인과 JWT(JSON Web Token)를 활용한 안전한 인증 방식에 대해 알아보겠습니다.
특히 백엔드 중심의 소셜 로그인 구현 방식을 쉽게 설명해 드리겠습니다.


먼저 알면 좋은 것

소셜 로그인이란?

소셜 로그인은 Google, Kakao, Facebook 등의 소셜 미디어 계정을 이용해 다른 웹사이트나 애플리케이션에 로그인하는 방법입니다.
사용자는 새로운 계정을 만들지 않아도 되고, 서비스 제공자는 사용자 인증을 소셜 플랫폼에 위임할 수 있어 편리합니다.

JWT란 무엇인가?

JWT(JSON Web Token)는 당사자 간에 정보를 안전하게 전송하기 위한 컴팩트하고 독립적인 방식을 정의하는 개방형 표준입니다.
세 부분으로 구성됩니다:

  1. 헤더(Header): 토큰 유형과 사용된 알고리즘 정보
  2. 페이로드(Payload): 사용자 ID 등 클레임(claim) 정보
  3. 서명(Signature): 토큰이 변조되지 않았는지 확인하는 서명

HTTP-Only 쿠키란?

HTTP-Only 쿠키는 JavaScript를 통해 접근할 수 없는 특수한 쿠키입니다. document.cookie로 읽거나 수정할 수 없으며, 오직 HTTP 요청을 통해서만 서버로 전송됩니다. 이 특성은 XSS(Cross-Site Scripting) 공격으로부터 쿠키를 보호하는 중요한 보안 장치입니다.

Reissue(재발급)란?

"Reissue"는 "재발급"을 의미합니다. JWT 인증에서는 보안을 위해 액세스 토큰의 유효기간을 짧게 설정합니다. 액세스 토큰이 만료되면 새로운 토큰을 발급받아야 하는데, 이 과정을 "토큰 재발급(Token Reissue)"이라고 합니다.

우리 구현에서 /reissue 경로는 리프레시 토큰을 이용해 새로운 액세스 토큰을 발급받는 중간 단계를 담당합니다. 소셜 로그인 후 리프레시 토큰만 받은 상태에서 액세스 토큰을 요청하는 과정이라고 볼 수 있습니다.


백엔드 중심 소셜 로그인 + JWT 인증 흐름

이제 전체 인증 흐름을 단계별로 자세히 살펴보겠습니다:

1. 소셜 로그인 버튼 클릭

사용자가 웹사이트에서 "구글로 로그인" 또는 "카카오로 로그인" 버튼을 클릭합니다. 이 버튼은 일반적인 AJAX 요청이 아닌 하이퍼링크 형태로 구현되어 있습니다.

// 예시 코드
function handleGoogleLogin() {
  window.location.href = 'http://api.myservice.com/login/google';
}

2. 백엔드에서 소셜 인증 처리

사용자가 백엔드 URL로 이동하면, 백엔드 서버는 다음과 같은 작업을 수행합니다:

  1. 소셜 로그인 제공자(Google, Kakao 등)의 로그인 페이지로 사용자를 리다이렉트
  2. 사용자가 소셜 플랫폼에서 로그인하고 권한을 승인
  3. 소셜 플랫폼이 백엔드로 인증 코드(Authorization code)를 전달
  4. 백엔드는 이 코드를 사용해 사용자 정보를 소셜 플랫폼으로부터 받아옴

이 과정은 모두 백엔드에서 처리되므로 프론트엔드에서는 별도의 OAuth 라이브러리나 설정이 필요 없습니다.

3. 리프레시 토큰 생성 및 저장

백엔드는 소셜 플랫폼에서 받은 사용자 정보를 확인하고 인증 토큰을 생성합니다:

  1. 사용자 정보(이메일, 이름 등)를 기반으로 리프레시 토큰 생성
  2. 이 리프레시 토큰을 HTTP-Only 쿠키에 저장
    Set-Cookie: refresh_token=eyJhbGciOiJIUzI1NiIsInR5...; HttpOnly; Secure; Path=/; Max-Age=604800
  3. 프론트엔드의 특정 URL(예: /reissue)로 사용자를 리다이렉트

HTTP-Only 쿠키를 사용하는 이유는 JavaScript로 접근할 수 없게 하여 XSS 공격으로부터 보호하기 위함입니다.

4. 액세스 토큰 요청

사용자가 /reissue 페이지에 도착하면, 자동으로 백엔드 API를 호출하여 액세스 토큰을 요청합니다:

// /reissue 페이지의 코드
useEffect(() => {
  async function getAccessToken() {
    try {
      const response = await axios.get('http://api.myservice.com/api/jwt/access-token', {
        withCredentials: true // 쿠키를 포함해서 요청
      });
      
      // 응답 처리
      if (response.headers.authorization) {
        const accessToken = response.headers.authorization.replace('Bearer ', '');
        // 액세스 토큰 저장
        localStorage.setItem('accessToken', accessToken);
        // 메인 페이지로 이동
        window.location.href = '/main';
      }
    } catch (error) {
      console.error('액세스 토큰 요청 실패', error);
      window.location.href = '/login'; // 오류 시 로그인 페이지로 이동
    }
  }
  
  getAccessToken();
}, []);

여기서 withCredentials: true 설정은 CORS 요청에서 쿠키를 포함시키기 위한 중요한 옵션입니다.

5. 백엔드에서 액세스 토큰 발급

백엔드는 요청에 포함된 리프레시 토큰을 검증하고 액세스 토큰을 발급합니다:

  1. 쿠키에서 리프레시 토큰 추출 및 검증
  2. 유효한 토큰이면 짧은 유효기간(보통 15분~1시간)을 가진 액세스 토큰 생성
  3. 액세스 토큰을 응답 헤더에 포함시켜 반환
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

6. 인증 완료 및 서비스 이용

프론트엔드는 응답 헤더에서 액세스 토큰을 추출하여 저장하고, 이후 API 요청 시 이 토큰을 사용합니다:

// API 요청 예시
async function fetchUserData() {
  const accessToken = localStorage.getItem('accessToken');
  
  const response = await axios.get('http://api.myservice.com/api/user/profile', {
    headers: {
      Authorization: `Bearer ${accessToken}`
    }
  });
  
  return response.data;
}

왜 이 방식이 안전한가?

이 인증 방식의 보안상 장점은 다음과 같습니다:

  1. 리프레시 토큰 보호: HTTP-Only 쿠키를 사용하여 JavaScript에서 리프레시 토큰에 접근할 수 없음
  2. 액세스 토큰의 짧은 수명: 액세스 토큰이 탈취되더라도 짧은 시간 내에 만료됨
  3. 토큰 분리: 리프레시 토큰과 액세스 토큰을 분리하여 관리함으로써 보안 강화
  4. 백엔드 중심 OAuth: 민감한 OAuth 인증 과정을 백엔드에서 처리하여 클라이언트 측 보안 취약점 감소

/reissue 경로의 역할

/reissue 경로는 소셜 로그인 후 백엔드에서 프론트엔드로 리다이렉트되는 중간 지점입니다. 이 페이지의 주요 역할은:

  1. 소셜 로그인 후 리프레시 토큰만 받은 상태에서 액세스 토큰을 요청
  2. 리프레시 토큰은 쿠키에 저장되어 있어 JavaScript로 직접 접근 불가능
  3. 액세스 토큰을 받아 저장한 후, 실제 서비스 페이지로 리다이렉트

이 과정이 "재발급(reissue)"이라 불리는 이유는, 리프레시 토큰을 사용해 액세스 토큰을 새로 발급받는 과정이기 때문입니다. 소셜 로그인 직후에는 리프레시 토큰만 있고 액세스 토큰은 없는 상태이므로, 이 과정을 통해 첫 액세스 토큰을 발급받게 됩니다.

구현 시 주의사항

  1. CORS 설정: 백엔드에서 적절한 CORS 설정이 필요합니다.

    Access-Control-Allow-Origin: https://yourfrontend.com
    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Headers: Content-Type, Authorization
  2. 쿠키 설정: 보안을 위해 SameSite, Secure 옵션도 설정하는 것이 좋습니다.

    Set-Cookie: refresh_token=token; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800
  3. 에러 처리: 토큰 만료, 인증 실패 등 다양한 경우에 대한 처리가 필요합니다.

결론

백엔드 중심의 소셜 로그인과 JWT를 활용한 인증 방식은 프론트엔드의 복잡성을 줄이면서도 높은 보안성을 제공합니다. 리프레시 토큰은 HTTP-Only 쿠키로 안전하게 보관하고, 액세스 토큰은 짧은 유효기간을 가지고 있어 보안 위험을 최소화합니다.

이 방식은 특히 다양한 소셜 로그인을 지원해야 하는 서비스에서 프론트엔드 코드의 중복을 줄이고, OAuth 관련 설정과 보안 이슈를 백엔드에서 집중적으로 관리할 수 있게 해줍니다.

웹 애플리케이션을 개발할 때 인증 시스템은 가장 중요한 부분 중 하나입니다. 이 글에서 설명한 방식을 참고하여 안전하고 사용자 친화적인 인증 시스템을 구현하시기 바랍니다!

profile
궁금한 것, 했던 것, 시행착오 그리고 기억하고 싶은 것들을 기록합니다.

0개의 댓글