기존 JWT 로그인 시스템에 소셜로그인(OAuth 2.0)을 통합하는 방법

msw-Hub·2025년 6월 15일

springboot

목록 보기
2/4
post-thumbnail

Spring Security와 JWT를 활용한 로그인 방식은 기본적으로 일반 로그인에 초점이 맞춰져 있다.
하지만 최근 많은 서비스들이 일반 로그인 없이 소셜 로그인만으로도 회원 관리를 진행하는 경우가 많다.
그래서 소셜 로그인을 어떻게 구현하는지, 그리고 기존의 일반 로그인 방식에 소셜 로그인을 어떻게 통합할 수 있을지에 대해 공부해보았다.


🔐 일반로그인 방식

일반 로그인 방식은 사용자로부터 아이디(또는 이메일)와 비밀번호를 받아 인증을 수행한다.
Spring Security와 JWT를 이용한 일반 로그인의 핵심 흐름과 설정은 다음과 같다.

이는 프론트엔드(React 등)에서 로그인 폼을 따로 구성하고, 그 값을 API로 백엔드(Spring)에 보내는 방식을 기반으로, Spring Security 기본 로그인이 아니다.


인증 과정 흐름(엑세스토큰 및 쿠기 기반)

  • 사용자가 로그인 폼에서 이메일과 비밀번호를 입력하여 로그인 요청을 보낸다.
  • 요청 본문(JSON)에서 이메일과 비밀번호를 추출하여 UsernamePasswordAuthenticationToken으로 감싼 후 인증을 시도한다.
  • AuthenticationManager가 내부적으로 UserDetailsService를 통해 DB에서 사용자 정보를 조회하고,
    PasswordEncoder로 비밀번호 일치 여부를 검증한다.
  • 인증이 성공하면 사용자 정보를 기반으로 JWT 토큰을 생성하고, 해당 토큰에 포함된 JTI 값을 Redis에 저장하여 유효한 토큰임을 기록한다.
  • 생성된 JWT는 HttpOnly, Secure 쿠키로 응답에 포함되어 클라이언트에 전달된다.
  • 이후 클라이언트는 해당 쿠키를 자동으로 포함하여 요청을 보낸다.
  • 요청이 들어올 때마다 서버의 필터는 쿠키에서 JWT를 추출하고,
    유효성 검증(JWT 서명, 만료시간, Redis 블랙리스트 여부 등)을 통해 인증 여부를 판단한다.
  • 토큰이 유효하면 인증 객체를 생성하여 Spring Security의 SecurityContext에 저장하고,
    이후 컨트롤러와 서비스에서 인증된 사용자로 요청을 처리할 수 있다.

흐름을 요약하면 다음과 같다.

[클라이언트 로그인 요청: POST /api/auth/login]
    ↓
[JwtLoginFilter]
    - JSON 형태의 이메일/비밀번호 추출
    - ↓
      [AuthenticationManager + UserDetailsService]
        → DB에서 사용자 정보 조회
        → 비밀번호 검증
    - 인증 성공
    - ↓
      JWT 생성
        → 이메일, 역할 등 정보 포함
        → 고유 식별자(JTI) 부여
        → Redis에 JTI 저장 (유효 토큰으로 간주)
    - ↓
      응답
        → JWT를 HttpOnly + Secure 쿠키에 저장
        → {"message": "로그인 성공"} JSON 응답

    ↓
[클라이언트 → JWT 포함하여 이후 요청 자동 전송 (쿠키)]

    ↓
[JwtFilter]
    - 매 요청마다 실행되는 필터
    - ↓
      ① Cookie에서 JWT 추출
      ② JWT 유효성 검증 (서명, 만료 등)
      ③ Redis에서 블랙리스트 여부 확인 (JTI 기준)
      ④ 유효한 경우:
          - JWT에서 사용자 정보(email, role) 추출
          - 인증 객체 생성 후 SecurityContext에 저장

    ↓
[요청 처리: Controller, Service 등]
    - Spring Security 인증 상태 유지

🌐 소셜 로그인 방식

소셜 로그인을 통해 사용자는 별도의 회원가입 절차 없이 편리하게 로그인할 수 있으며, 기존 일반 로그인 방식과 병행하거나 대체하기도 한다.


1. 소셜 로그인 동작 원리

  1. 사용자가 소셜 로그인 버튼을 클릭하면, 프론트는 소셜 인증 서버(예: Google OAuth 서버)로 인증 요청을 보낸다.
  2. 소셜 인증 서버에서 사용자 동의를 받고 인증이 완료되면, 인증 토큰(Authorization Code 또는 Access Token)을 프론트에 반환한다.
  3. 프론트는 이 토큰을 백엔드 서버에 전달한다.
  4. 백엔드 서버는 토큰을 검증하고, 소셜 API를 호출하여 사용자 정보를 조회한다.
  5. 소셜 계정의 고유 식별자(예: Google의 sub, 카카오의 id)를 기반으로 DB에서 기존 사용자 존재 여부를 확인한다.
    5-1. 사용자가 이미 가입한 경우, 기존 계정과 연동하여 로그인 처리한다.
    5-2. 신규 사용자라면 소셜 계정 정보를 토대로 회원가입 처리를 수행한다.
  6. 이후 일반 로그인과 동일하게 JWT 토큰을 생성하여 프론트에 전달하고, 인증 세션을 유지한다.

2. DB에서의 사용자 테이블 차이

  1. 통합 사용자 테이블 (User Entity)
  • 모든 사용자의 공통 정보를 저장하는 기본 테이블이다.
  • 이메일, 이름, 권한, 가입일 등 일반적인 사용자 정보를 포함한다.
  • 일반 로그인 사용자와 소셜 로그인 사용자 모두 이 테이블에 등록된다.
  • 일반 로그인 사용자의 경우 비밀번호 필드를 포함한다.
  1. 소셜 로그인 정보 별도 테이블 (SocialUser Entity)
  • 소셜 로그인 제공자별 고유 ID, 액세스 토큰 등 소셜 로그인 관련 정보를 별도로 저장한다.
  • 이 테이블은 기본 사용자 테이블(User)의 기본키를 외래키로 참조한다.
  • 소셜 로그인 제공자 구분을 위한 필드(provider)와 소셜 고유 ID(provider_user_id)를 포함한다.
  • 여러 소셜 로그인 계정을 하나의 사용자에 연결할 수 있도록 1:N 관계로 설계 가능하다.

3. 소셜로그인은 기존 로그인방식 어디에 넣어야하는가?

소셜 로그인은 일반 로그인과 인증 흐름 및 요청 방식이 다르므로, 일반 로그인과 같은 필터에서 처리하지 않고 별도의 필터 또는 컨트롤러 엔드포인트를 둔다.

상황에 따라 처리 방식이 다르다.

  • 백엔드가 OAuth 제공자의 콜백 요청을 직접 받는 경우 (예: GET /oauth2/callback)
    → 이 경우 백엔드가 인증 코드 교환 및 사용자 정보 조회를 직접 처리하므로,
    → 별도의 소셜 로그인 전용 필터(SocialLoginFilter)를 등록하여 해당 엔드포인트 요청을 가로채 처리한다.
    → 이 필터는 일반 로그인용 JwtLoginFilter와 별개로 동작하며, 소셜 로그인 인증 후 JWT 토큰을 생성하여 응답한다.

  • 프론트엔드가 OAuth 인증 코드를 받아 처리하는 경우 (예: React SPA에서 코드 받아 POST /api/auth/social-login 요청)
    → 이 경우 백엔드는 인증 코드 교환, 사용자 DB 연동, JWT 생성 등의 작업을 컨트롤러에서 처리한다.
    → 별도의 필터를 사용하지 않고 컨트롤러 메서드에서 API 요청을 처리한다.

주로, 서비스는 후자의 경우로 개발되기에, 후자의 간단한 흐름은 다음과 같다.

[프론트 소셜 로그인 요청: POST /api/auth/social-login { code }]
    ↓
[AuthController.socialLogin()]
    - OAuth 인증 코드 처리
    - 소셜 사용자 정보 조회
    - 사용자 DB 연동 및 가입 처리
    - JWT 생성 및 응답 (JSON 바디나 헤더)
    ↓
[프론트 → JWT 포함하여 이후 요청 자동 전송]

    ↓
[JwtFilter]
    - 모든 요청에 대해 JWT 유효성 검증
    - 인증 성공 시 SecurityContext에 사용자 정보 저장

    ↓
[요청 처리: Controller, Service 등]
    - 인증된 사용자로 요청 처리

4. 소셜로그인을 통한 회원가입 로직은?

소셜로그인 요청이 들어오면, 먼저 소셜 제공자(예: 구글, 카카오)에서 사용자 정보를 받아온다.
그다음, 우리 DB에 이 소셜 사용자가 이미 가입된 회원인지 확인한다.

  • 이미 가입된 회원이라면, 바로 로그인 처리해서 JWT 토큰을 발급한다.
  • 가입된 회원이 아니라면, 새로 회원 정보를 저장하고 회원가입을 진행한 뒤 로그인 처리하고 JWT 토큰을 발급한다.

즉, 소셜로그인 요청 하나로 회원가입과 로그인을 함께 처리한다.

흐름은 다음과 같다:

[프론트 소셜 로그인 요청]  
    ↓  
[서버가 소셜 제공자로부터 사용자 정보 획득]  
    ↓  
[DB에서 소셜 계정 가입 여부 확인]  
    ↓  
가입 회원이면 → [로그인 처리 → JWT 생성 → 응답]  
가입 회원이 아니면 → [신규 회원 DB 저장 → 로그인 처리 → JWT 생성 → 응답]  

4-1. 회원가입할때, 개인동의화면은 어떻게 동작하는가?

소셜 로그인 동의 화면(예: 카카오, 구글에서 개인정보 접근 동의)을 언제 보여줄지는 우리 서버(백엔드)가 직접 판단하는 게 아니다.
이 화면은 소셜 로그인 제공자(카카오, 구글)가 처음 로그인 시점에 기본적으로 또는 사용자가 이전에 동의를 취소했거나 권한이 없을 때 자동으로 보여준다.

흐름을 자세히 보면:

  1. 사용자가 프론트에서 ‘카카오 로그인’ 버튼 클릭
  2. 프론트는 카카오 OAuth 인증 URL로 리다이렉트하거나 팝업을 띄워서 카카오 인증 서버에 접근
  3. 카카오 인증 서버가 사용자 세션, 이전 동의 기록을 확인
  4. 이전에 동의한 적이 없거나 권한이 만료된 경우 → 동의 화면을 자동으로 보여줌
  5. 이미 동의가 되어 있으면 바로 승인(토큰 발급) 진행
  6. 사용자가 동의 완료 후, 카카오 인증 서버가 액세스 토큰을 프론트에 전달
  7. 프론트가 이 토큰을 백엔드 서버로 전달
  8. 백엔드는 이 토큰으로 사용자 정보 조회 후, 우리 DB에 있으면 로그인 처리, 없으면 회원가입 처리 요청

즉,
우리 서버가 유저 정보 존재 여부를 알고 동의 화면을 보여주는 게 아니다.
동의 화면은 ‘소셜 로그인 제공자’가 판단하여 보여준다.
우리 서버는 소셜 제공자가 발급한 토큰으로만 ‘사용자 존재 여부’를 판단한다.

흐름 요약:

[프론트에서 소셜 로그인 버튼 클릭]
    ↓
[소셜 로그인 제공자(OAuth 서버) 인증 화면]
    - 사용자 세션과 이전 동의 기록 확인
    - 동의 필요 시 동의 화면 자동 표시
    ↓
[사용자가 동의 완료]
    ↓
[소셜 로그인 제공자 → 액세스 토큰 발급]
    ↓
[프론트 → 액세스 토큰을 백엔드 서버에 전달]
    ↓
[백엔드 서버]
    - 토큰으로 소셜 제공자 API 호출하여 사용자 정보 조회
    - DB에서 해당 사용자 존재 여부 확인
        → 존재하면 로그인 처리
        → 없으면 회원가입 처리 후 로그인

4-2. 이미 회원가입을 한 상태에서, 다른 일반/소셜 로그인을 한다면? (계정 연동 관련)

계정 생성 방식(일반 or 소셜)에 따라 연동 로직이 달라진다.

  • ① 일반 로그인 → 소셜 로그인 연동

    • 소셜 로그인 시 제공된 이메일로 User 테이블을 조회한다.
    • 존재하면, 해당 user_id를 외래키로 갖는 SocialUser를 생성해 연동한다.
    • 이 구조로 여러 소셜 계정 연동이 가능하다.
  • ② 소셜 로그인 → 일반 로그인 연동

    • 최초 소셜 로그인 시, User를 먼저 생성하고 비밀번호는 null로 둔다.
    • 이후 일반 로그인으로 회원가입 시, 같은 이메일로 User를 조회한다.
    • 비밀번호가 null이면 기존 소셜 가입자이므로, 비밀번호를 설정해 연동한다.
  • ③ 소셜 로그인 → 다른 소셜 로그인 연동

    • 로그인 시 이메일로 User를 조회하여 user_id를 확인하고,
    • 해당 ID로 새로운 SocialUser를 추가하면 된다.

5. 소셜로그인 총 정리

  • 소셜 로그인은 일반 로그인과 달리 별도의 인증 과정을 거친다.
    일반 로그인은 Spring Security 필터에서 처리되지만, 소셜 로그인은 별도의 컨트롤러와 서비스에서 관리된다.

  • 소셜 로그인은 회원가입과 로그인을 하나의 흐름으로 처리한다.

  • 개인정보 동의는 소셜 로그인 제공자가 담당하며, 우리 서버는 발급받은 토큰을 통해 사용자 존재 여부만 판단한다.

  • 일반 로그인 계정과 소셜 로그인 계정을 이메일 기준으로 연동하여, 사용자가 다양한 로그인 방식을 혼합해서 사용할 수 있도록 지원한다.

  • OAuth 제공자의 인증 과정을 거쳐 토큰을 받고, 백엔드에서 토큰 검증 후 소셜 API를 호출해 사용자 정보를 조회한다.

흐름 요약:

[사용자] → 소셜 로그인 버튼 클릭 → 소셜 로그인 제공자 인증 및 개인정보 동의
    ↓
[프론트엔드] → 인증 코드 또는 Access Token을 백엔드 API로 전송
    ↓
[백엔드]
    ① 인증 코드로 Access Token 교환 (필요 시)
    ② Access Token으로 소셜 API 호출하여 사용자 정보 조회
    ③ DB에서 사용자 존재 여부 판단 (기존 회원/신규 회원)
    ④ 신규 회원이면 User와 SocialUser 정보 저장
    ⑤ JWT 토큰 생성 후 응답
    ↓
[클라이언트] → JWT를 HttpOnly 쿠키에 저장 → 이후 요청 시 쿠키 포함
    ↓
[서버] → JwtFilter가 JWT 검증 후 인증 처리 → 서비스 요청 처리
profile
천천히 시작하는 개발자

0개의 댓글