- Published on
Spring Security OAuth2 로그인 무한 리다이렉트 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서드파티 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단계: 브라우저에서 “쿠키가 유지되는지”부터 확인
무한 리다이렉트는 대부분 인증 결과를 저장할 세션이 유지되지 않아서 발생합니다.
체크 포인트
- IdP에서 돌아온 직후 응답에
Set-Cookie: JSESSIONID=...가 있는가 - 다음 요청에서
Cookie: JSESSIONID=...가 다시 전송되는가 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: httpsX-Forwarded-HostX-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이 정확히 일치하는지
- 스킴이
http와https로 섞이지 않는지 - 호스트가
example.com과www.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=None 과 Secure 설정이 어긋나면 OAuth2 리다이렉트가 반복됩니다.
비슷한 결의 “프록시/인증 계층에서 403이 나는데 WAF가 아니었다” 같은 실전 점검 패턴은 아래 글도 참고할 만합니다.
최종 체크리스트(10분 컷)
- 쿠키
Set-Cookie가 내려오는가- 다음 요청에 쿠키가 실리는가
- HTTPS인데
Secure누락/불일치가 없는가 - 크로스 사이트면
SameSite정책이 막고 있지 않은가
- 프록시
X-Forwarded-Proto가https로 전달되는가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”를 점검하면, 원인을 재현 가능한 형태로 좁히고 안정적으로 해결할 수 있습니다.