Published on

Spring Security OAuth2 로그인 무한 리다이렉트 해결

Authors

서드파티 OAuth2 로그인(구글, 카카오, 네이버 등)을 Spring Security로 붙인 뒤, 로그인 버튼을 누르면 IdP로 갔다가 돌아오긴 하는데 다시 로그인 페이지로 튕기거나 /oauth2/authorization/.../login/oauth2/code/... 사이를 끝없이 왕복하는 경우가 있습니다. 이 현상은 “인증이 성공했는데도 애플리케이션이 인증 상태를 유지하지 못하는” 전형적인 패턴이며, 원인은 대개 세션/쿠키가 저장되지 않거나, redirect URI 및 프록시/도메인 인식이 어긋났거나, Security 설정이 콜백을 다시 인증 대상으로 잡아버렸거나 셋 중 하나입니다.

이 글에서는 무한 리다이렉트를 증상 기반으로 분류하고, 로그/헤더/설정에서 무엇을 확인해야 하는지와 함께 바로 붙여 쓸 수 있는 코드 예제로 정리합니다.

관련해서 redirect URI 자체가 맞지 않아 루프가 생기는 케이스는 아래 글도 함께 보면 원인 좁히는 데 도움이 됩니다.


무한 리다이렉트의 대표 증상 4가지

1) IdP에서 콜백까지는 오는데 다시 로그인 페이지로 이동

  • 브라우저 네트워크 탭에서 302가 반복
  • 콜백 엔드포인트(/login/oauth2/code/...) 응답 후 /login 또는 /oauth2/authorization/... 로 다시 이동
  • 서버 로그에선 AnonymousAuthenticationToken 으로 처리되거나, SavedRequest 가 계속 남아 있음

핵심 원인 후보

  • 세션 쿠키(JSESSIONID)가 저장/전송되지 않음
  • SameSite/Secure/도메인/경로 문제
  • HTTPS 종단이 로드밸런서(ALB/Nginx)인데 앱이 HTTP로 인식

2) 콜백에서 state 검증 실패로 다시 인증 시작

  • 로그에 InvalidStateParameterException 또는 AuthorizationRequest not found 계열
  • 분산 환경에서 특히 자주 발생

핵심 원인 후보

  • HttpSessionOAuth2AuthorizationRequestRepository 가 세션에 저장한 요청이 콜백 시점에 사라짐
  • 서버가 여러 대인데 세션 스티키/공유가 안 됨
  • 쿠키가 막혀 세션이 유지되지 않음

3) /login/oauth2/code/... 자체가 보호되어 인증을 요구

  • 콜백 URL이 permitAll 되지 않아 Security가 다시 인증 플로우를 시작

핵심 원인 후보

  • authorizeHttpRequests 규칙이 잘못되어 콜백이 인증 대상이 됨

4) 성공 핸들러에서 잘못된 URL로 리다이렉트 → 다시 인증 필요

  • 성공 후 프론트 URL로 보냈는데, 그 URL이 다시 401/302를 유발

핵심 원인 후보

  • defaultSuccessUrl 또는 AuthenticationSuccessHandler 가 보호된 경로로 보냄
  • SPA 라우팅/프록시 설정으로 인해 서버가 다른 호스트로 리다이렉트

1단계: 브라우저에서 “쿠키가 유지되는지”부터 확인

무한 리다이렉트는 대부분 인증 결과를 저장할 세션이 유지되지 않아서 발생합니다.

체크 포인트

  1. IdP에서 돌아온 직후 응답에 Set-Cookie: JSESSIONID=... 가 있는가
  2. 다음 요청에서 Cookie: JSESSIONID=... 가 다시 전송되는가
  3. Set-Cookie 속성에 아래가 맞는가
  • Secure (HTTPS라면 필요)
  • SameSite (크로스 사이트 리다이렉트면 정책 영향)
  • Domain (서브도메인/루트 도메인 불일치 주의)
  • Path (보통 /)

Spring Boot에서 쿠키 속성 빠르게 조정

application.yml 예시입니다.

server:
  servlet:
    session:
      cookie:
        name: JSESSIONID
        same-site: none
        secure: true
        path: /

주의할 점

  • same-site: none 을 쓰면 브라우저 정책상 secure: true 가 사실상 필수입니다.
  • 로컬 개발에서 HTTPS가 아니라면 SameSite=None + Secure=true 조합이 쿠키를 아예 못 굽는 상황이 생길 수 있습니다. 로컬은 HTTP로 두고 운영만 HTTPS로 분기하거나, 로컬도 TLS를 붙이세요.

2단계: 프록시(로드밸런서) 뒤에서 “HTTPS 인식 불일치” 잡기

ALB/Nginx/Ingress 뒤에서 TLS를 종료하고 앱은 HTTP로 받는 구성에서, 앱이 스스로를 HTTP로 인식하면 다음 문제가 연쇄적으로 발생합니다.

  • Spring Security가 리다이렉트 URL을 HTTP로 생성
  • 실제 브라우저는 HTTPS로 접근 중
  • 쿠키 Secure 처리/리다이렉트/redirect URI 계산이 꼬여 세션이 유지되지 않거나 redirect URI mismatch로 루프

해결: Forwarded 헤더 처리 활성화

server:
  forward-headers-strategy: framework

또는 환경에 따라 native 가 더 맞을 때도 있습니다.

server:
  forward-headers-strategy: native

그리고 프록시가 아래 헤더를 제대로 넘기는지 확인합니다.

  • X-Forwarded-Proto: https
  • X-Forwarded-Host
  • X-Forwarded-Port

Nginx 예시(필요 시)

proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;

3단계: 콜백 URL과 OAuth2 엔드포인트를 permitAll로 열기

콜백 엔드포인트가 인증을 요구하면, 콜백을 처리하기 전에 다시 인증을 시작해 루프가 됩니다.

Spring Security 6 기준 설정 예시입니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(
                    "/",
                    "/login",
                    "/error",
                    "/oauth2/**",
                    "/login/oauth2/**"
                ).permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(Customizer.withDefaults());

        return http.build();
    }
}

포인트

  • "/oauth2/**" 는 authorization endpoint(/oauth2/authorization/...)가 포함됩니다.
  • "/login/oauth2/**" 는 콜백(/login/oauth2/code/...)이 포함됩니다.
  • 커스텀 콜백 경로를 쓰면 그 경로도 반드시 permitAll 해야 합니다.

4단계: 분산 환경(서버 여러 대)에서 세션/AuthorizationRequest 저장소 문제 해결

state 검증 실패나 AuthorizationRequest not found 는 “처음 요청을 저장한 곳”과 “콜백을 처리하는 곳”이 달라서 생깁니다.

대표적인 상황

  • 서버가 2대 이상인데 스티키 세션이 없음
  • 세션을 로컬 메모리에만 저장
  • 쿠키가 막혀 세션이 새로 생성

선택지 A: 로드밸런서 스티키 세션

  • 가장 빠른 응급처치
  • 다만 장기적으로는 세션 공유(예: Redis)나 stateless 설계를 고려

선택지 B: Spring Session + Redis로 세션 공유

의존성(Gradle)

implementation "org.springframework.session:spring-session-data-redis"
implementation "org.springframework.boot:spring-boot-starter-data-redis"

설정 예시

spring:
  session:
    store-type: redis
  data:
    redis:
      host: localhost
      port: 6379

이렇게 하면 HttpSessionOAuth2AuthorizationRequestRepository 가 사용하는 세션이 노드 간 공유되어 콜백 처리 시점에도 state 를 찾을 확률이 크게 올라갑니다.


5단계: redirect URI/베이스 URL 불일치 점검

무한 리다이렉트는 redirect URI mismatch가 “에러 페이지로 끝나지 않고” 다시 인증 시작으로 연결될 때도 발생합니다.

점검 목록

  • IdP 콘솔에 등록한 redirect URI와 실제 콜백 URL이 정확히 일치하는지
  • 스킴이 httphttps 로 섞이지 않는지
  • 호스트가 example.comwww.example.com 처럼 미세하게 다른지
  • 경로가 /login/oauth2/code/google 처럼 provider id까지 포함해 맞는지

Spring Boot 기본 콜백 패턴은 보통 "/login/oauth2/code/{registrationId}" 입니다. 문서나 코드에 제네릭 표기 같은 걸 적을 때는 반드시 인라인 코드로 감싸서(예: "{registrationId}") MDX 빌드 에러도 피하세요.

문제 유형별로 redirect URI를 빠르게 정리하는 방법은 아래 글도 참고하세요.


6단계: 성공 리다이렉트가 다시 보호 자원으로 떨어지는지 확인

로그인이 성공했는데 성공 후 이동한 URL이 다시 401/302를 유발하면, 사용자는 “로그인이 안 된 것처럼” 느끼고 루프처럼 보입니다.

흔한 실수

  • 성공 후 "/" 로 보냈는데, 실제로는 "/" 가 인증 필요
  • SPA에서 "/app" 로 보냈는데 서버 라우팅이 404 또는 다시 로그인으로 리다이렉트

해결: 성공 URL을 명확히 지정

http
  .oauth2Login(oauth2 -> oauth2
    .defaultSuccessUrl("/home", true)
  );

혹은 커스텀 성공 핸들러에서 조건별 분기

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import java.io.IOException;

public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        response.sendRedirect("/home");
    }
}

성공 핸들러가 프론트 도메인으로 리다이렉트해야 한다면, 그 도메인에서 세션 쿠키가 유효한지(도메인/secure/samesite)도 같이 확인해야 합니다.


7단계: 디버그 로그로 “어디서 익명으로 떨어지는지” 추적

무한 리다이렉트는 추측으로 고치기 어렵습니다. Spring Security 로그를 켜고, 어떤 필터에서 인증이 사라지는지 확인하면 시간이 확 줄어듭니다.

application.yml

logging:
  level:
    org.springframework.security: DEBUG

관찰 포인트

  • 콜백 처리 직후 SecurityContext 가 세션에 저장되는지
  • 다음 요청에서 SecurityContext 를 세션에서 로드하는지
  • SavedRequest 가 계속 남아 같은 URL로 반복 이동하는지

특히 JSESSIONID 가 요청마다 바뀐다면(매 요청 새 세션) 쿠키/프록시/HTTPS 인식 문제일 확률이 매우 높습니다.


운영 환경에서 자주 터지는 조합: ALB/Ingress + HTTPS + SameSite

EKS 같은 환경에서 ALB Ingress를 쓰면, 애플리케이션은 HTTP로 받고 외부는 HTTPS인 구성이 흔합니다. 이때 Forwarded 헤더 처리가 빠져 있거나, SameSite=NoneSecure 설정이 어긋나면 OAuth2 리다이렉트가 반복됩니다.

비슷한 결의 “프록시/인증 계층에서 403이 나는데 WAF가 아니었다” 같은 실전 점검 패턴은 아래 글도 참고할 만합니다.


최종 체크리스트(10분 컷)

  • 쿠키
    • Set-Cookie 가 내려오는가
    • 다음 요청에 쿠키가 실리는가
    • HTTPS인데 Secure 누락/불일치가 없는가
    • 크로스 사이트면 SameSite 정책이 막고 있지 않은가
  • 프록시
    • X-Forwarded-Protohttps 로 전달되는가
    • server.forward-headers-strategy 를 켰는가
  • Security 규칙
    • "/oauth2/**", "/login/oauth2/**"permitAll 인가
  • 분산
    • 스티키 세션 또는 Redis 세션 공유가 있는가
  • redirect URI
    • IdP 등록값과 실제 콜백 URL이 스킴/호스트/경로까지 완전 일치하는가
  • 성공 리다이렉트
    • 성공 후 URL이 다시 인증을 요구하지 않는가

마무리

Spring Security OAuth2 무한 리다이렉트는 겉보기엔 “로그인이 안 된다”이지만, 실제로는 인증 상태를 저장하는 매체(세션/쿠키)애플리케이션이 인식하는 외부 URL(프록시/HTTPS/redirect URI) 이 어긋나면서 발생하는 경우가 대부분입니다.

위 순서대로 “쿠키 유지 확인 → Forwarded 헤더/HTTPS 인식 → 콜백 permitAll → 분산 세션 → redirect URI”를 점검하면, 원인을 재현 가능한 형태로 좁히고 안정적으로 해결할 수 있습니다.