
Spring Security와 JWT를 활용한 로그인 방식은 기본적으로 일반 로그인에 초점이 맞춰져 있다.
하지만 최근 많은 서비스들이 일반 로그인 없이 소셜 로그인만으로도 회원 관리를 진행하는 경우가 많다.
그래서 소셜 로그인을 어떻게 구현하는지, 그리고 기존의 일반 로그인 방식에 소셜 로그인을 어떻게 통합할 수 있을지에 대해 공부해보았다.
일반 로그인 방식은 사용자로부터 아이디(또는 이메일)와 비밀번호를 받아 인증을 수행한다.
Spring Security와 JWT를 이용한 일반 로그인의 핵심 흐름과 설정은 다음과 같다.
이는 프론트엔드(React 등)에서 로그인 폼을 따로 구성하고, 그 값을 API로 백엔드(Spring)에 보내는 방식을 기반으로, Spring Security 기본 로그인이 아니다.
UsernamePasswordAuthenticationToken으로 감싼 후 인증을 시도한다. AuthenticationManager가 내부적으로 UserDetailsService를 통해 DB에서 사용자 정보를 조회하고,PasswordEncoder로 비밀번호 일치 여부를 검증한다.JTI 값을 Redis에 저장하여 유효한 토큰임을 기록한다. HttpOnly, Secure 쿠키로 응답에 포함되어 클라이언트에 전달된다.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 인증 상태 유지
소셜 로그인을 통해 사용자는 별도의 회원가입 절차 없이 편리하게 로그인할 수 있으며, 기존 일반 로그인 방식과 병행하거나 대체하기도 한다.
소셜 로그인은 일반 로그인과 인증 흐름 및 요청 방식이 다르므로, 일반 로그인과 같은 필터에서 처리하지 않고 별도의 필터 또는 컨트롤러 엔드포인트를 둔다.
상황에 따라 처리 방식이 다르다.
백엔드가 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 등]
- 인증된 사용자로 요청 처리
소셜로그인 요청이 들어오면, 먼저 소셜 제공자(예: 구글, 카카오)에서 사용자 정보를 받아온다.
그다음, 우리 DB에 이 소셜 사용자가 이미 가입된 회원인지 확인한다.
즉, 소셜로그인 요청 하나로 회원가입과 로그인을 함께 처리한다.
흐름은 다음과 같다:
[프론트 소셜 로그인 요청]
↓
[서버가 소셜 제공자로부터 사용자 정보 획득]
↓
[DB에서 소셜 계정 가입 여부 확인]
↓
가입 회원이면 → [로그인 처리 → JWT 생성 → 응답]
가입 회원이 아니면 → [신규 회원 DB 저장 → 로그인 처리 → JWT 생성 → 응답]
소셜 로그인 동의 화면(예: 카카오, 구글에서 개인정보 접근 동의)을 언제 보여줄지는 우리 서버(백엔드)가 직접 판단하는 게 아니다.
이 화면은 소셜 로그인 제공자(카카오, 구글)가 처음 로그인 시점에 기본적으로 또는 사용자가 이전에 동의를 취소했거나 권한이 없을 때 자동으로 보여준다.
흐름을 자세히 보면:
즉,
우리 서버가 유저 정보 존재 여부를 알고 동의 화면을 보여주는 게 아니다.
동의 화면은 ‘소셜 로그인 제공자’가 판단하여 보여준다.
우리 서버는 소셜 제공자가 발급한 토큰으로만 ‘사용자 존재 여부’를 판단한다.
흐름 요약:
[프론트에서 소셜 로그인 버튼 클릭]
↓
[소셜 로그인 제공자(OAuth 서버) 인증 화면]
- 사용자 세션과 이전 동의 기록 확인
- 동의 필요 시 동의 화면 자동 표시
↓
[사용자가 동의 완료]
↓
[소셜 로그인 제공자 → 액세스 토큰 발급]
↓
[프론트 → 액세스 토큰을 백엔드 서버에 전달]
↓
[백엔드 서버]
- 토큰으로 소셜 제공자 API 호출하여 사용자 정보 조회
- DB에서 해당 사용자 존재 여부 확인
→ 존재하면 로그인 처리
→ 없으면 회원가입 처리 후 로그인
계정 생성 방식(일반 or 소셜)에 따라 연동 로직이 달라진다.
① 일반 로그인 → 소셜 로그인 연동
User 테이블을 조회한다. user_id를 외래키로 갖는 SocialUser를 생성해 연동한다. ② 소셜 로그인 → 일반 로그인 연동
User를 먼저 생성하고 비밀번호는 null로 둔다. User를 조회한다. null이면 기존 소셜 가입자이므로, 비밀번호를 설정해 연동한다.③ 소셜 로그인 → 다른 소셜 로그인 연동
User를 조회하여 user_id를 확인하고, SocialUser를 추가하면 된다.소셜 로그인은 일반 로그인과 달리 별도의 인증 과정을 거친다.
일반 로그인은 Spring Security 필터에서 처리되지만, 소셜 로그인은 별도의 컨트롤러와 서비스에서 관리된다.
소셜 로그인은 회원가입과 로그인을 하나의 흐름으로 처리한다.
개인정보 동의는 소셜 로그인 제공자가 담당하며, 우리 서버는 발급받은 토큰을 통해 사용자 존재 여부만 판단한다.
일반 로그인 계정과 소셜 로그인 계정을 이메일 기준으로 연동하여, 사용자가 다양한 로그인 방식을 혼합해서 사용할 수 있도록 지원한다.
OAuth 제공자의 인증 과정을 거쳐 토큰을 받고, 백엔드에서 토큰 검증 후 소셜 API를 호출해 사용자 정보를 조회한다.
흐름 요약:
[사용자] → 소셜 로그인 버튼 클릭 → 소셜 로그인 제공자 인증 및 개인정보 동의
↓
[프론트엔드] → 인증 코드 또는 Access Token을 백엔드 API로 전송
↓
[백엔드]
① 인증 코드로 Access Token 교환 (필요 시)
② Access Token으로 소셜 API 호출하여 사용자 정보 조회
③ DB에서 사용자 존재 여부 판단 (기존 회원/신규 회원)
④ 신규 회원이면 User와 SocialUser 정보 저장
⑤ JWT 토큰 생성 후 응답
↓
[클라이언트] → JWT를 HttpOnly 쿠키에 저장 → 이후 요청 시 쿠키 포함
↓
[서버] → JwtFilter가 JWT 검증 후 인증 처리 → 서비스 요청 처리