- Published on
Spring Security OAuth2 리다이렉트 URI 불일치 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서드파티 로그인(Google, Kakao, GitHub 등)을 Spring Security OAuth2로 붙일 때 가장 흔한 장애가 redirect_uri 불일치입니다. 에러 메시지는 공급자마다 다르지만 본질은 같습니다.
redirect_uri_mismatchThe redirect_uri MUST match the registered callback URLinvalid_request: redirect_uri
이 글에서는 Spring Security가 실제로 어떤 redirect_uri를 만들어 보내는지, 그리고 그 값이 왜 환경(로컬/스테이징/운영, 프록시/ALB/Ingress)에서 달라지는지를 기준으로 원인을 분류하고 해결책을 제시합니다.
1) redirect_uri가 만들어지는 방식부터 이해하기
Spring Security의 OAuth2 Login은 기본적으로 아래 흐름으로 동작합니다.
/oauth2/authorization/{registrationId}로 진입OAuth2AuthorizationRequestRedirectFilter가 Authorization Request 생성- 이때
redirect_uri를 템플릿으로 구성해 Provider(Authorization Server)로 리다이렉트 - 로그인 후 Provider가
redirect_uri로 code를 붙여 콜백 - Spring Security가
/login/oauth2/code/{registrationId}를 처리
기본 redirect_uri 템플릿은 보통 다음 형태입니다.
{baseUrl}/login/oauth2/code/{registrationId}
문제는 여기서 baseUrl이 무엇으로 계산되느냐입니다. Spring Security는 요청의 스킴/호스트/포트/컨텍스트패스를 보고 baseUrl을 만들기 때문에, 프록시 뒤에서 http로 보이거나 포트가 바뀌면 redirect_uri가 달라집니다.
2) 가장 흔한 원인 6가지 (체크리스트)
(1) Provider에 등록한 콜백 URL과 1글자라도 다름
Provider 콘솔에 등록한 Callback URL과 실제 요청에 포함된 redirect_uri는 완전 일치해야 합니다.
- 트레일링 슬래시(
/) 유무 httpvshttps- 포트
:8080유무 - 경로 대소문자
- 쿼리스트링 포함 여부
해결의 시작은 “Spring이 실제로 보낸 redirect_uri”를 확인하는 것입니다.
(2) 로컬은 되는데 운영에서만 깨짐 (프록시/로드밸런서)
운영에서 TLS 종료를 ALB/Ingress가 하고, 애플리케이션은 내부에서 http로 받는 경우가 대표적입니다.
- 외부:
https://app.example.com - 내부(스프링이 보는 값):
http://app.example.com
그러면 Spring이 http://.../login/oauth2/code/... 를 redirect_uri로 만들어 보내고 Provider는 등록된 https://... 와 불일치로 거절합니다.
(3) X-Forwarded-* 헤더 미반영
프록시 뒤에서는 보통 다음 헤더로 원래 요청 정보를 전달합니다.
X-Forwarded-Proto: httpsX-Forwarded-Host: app.example.comX-Forwarded-Port: 443
하지만 Spring이 이를 신뢰하도록 설정하지 않으면 baseUrl 계산이 틀어집니다.
(4) context-path / 서브패스 배포
예: 애플리케이션이 /api 컨텍스트로 떠 있고, 외부는 /로 서비스하거나 반대로 매핑될 때 경로가 어긋납니다.
- 기대:
https://app.example.com/login/oauth2/code/google - 실제:
https://app.example.com/api/login/oauth2/code/google
Provider 등록값도 동일하게 맞춰야 합니다.
(5) registrationId 혼동
/login/oauth2/code/{registrationId} 의 {registrationId}는 spring.security.oauth2.client.registration.<id>의 <id>입니다.
예를 들어 google로 등록했는데 Provider에는 .../code/kakao로 등록하는 식의 실수가 발생합니다.
(6) Authorization Request를 커스터마이징하다 redirect_uri가 변형됨
AuthorizationRequestResolver를 커스터마이징하거나, redirectUri 템플릿을 직접 지정하는 과정에서 의도치 않게 값이 바뀌는 경우가 있습니다.
3) “실제 redirect_uri”를 빠르게 확인하는 방법
가장 빠른 방법은 브라우저 네트워크 탭에서 Provider로 날아가는 authorize URL을 보는 것입니다.
https://accounts.google.com/o/oauth2/v2/auth?...&redirect_uri=...
서버 로그로도 확인할 수 있습니다. Spring Security 디버그 로그를 켜면 도움이 됩니다.
# application.yml
logging:
level:
org.springframework.security: DEBUG
또는 필터에서 리다이렉트 직전 URL을 로깅할 수도 있습니다(운영에서는 민감정보 주의).
4) 정석 해결: Forwarded 헤더 처리 (프록시/ALB/Ingress)
운영에서 가장 “정답에 가까운” 해결은 Forwarded 헤더를 Spring이 신뢰하게 만드는 것입니다.
Spring Boot 3.x (Spring Framework 6) 권장 설정
# application.yml
server:
forward-headers-strategy: framework
이 설정은 X-Forwarded-* 또는 Forwarded 헤더를 기반으로 요청 스킴/호스트/포트를 재구성해, redirect_uri 생성에 반영합니다.
(참고) Boot 2.6~2.7에서 자주 쓰던 설정
환경에 따라 아래가 더 맞을 수 있습니다.
server:
forward-headers-strategy: native
다만 인프라(프록시)가 어떤 헤더를 넣는지에 따라 framework/native 중 맞는 값을 선택해야 합니다.
Ingress/ALB가 헤더를 제대로 넣는지 확인
Kubernetes Ingress(NGINX)나 AWS ALB Ingress Controller는 보통 자동으로 넣지만, 커스텀 설정으로 빠지는 경우가 있습니다. 이럴 땐 애플리케이션 파드에서 실제 요청 헤더를 덤프해 확인하는 것이 빠릅니다. (쿠버네티스 트러블슈팅 흐름은 EKS에서 kubectl exec·logs가 안 될 때 진단법 같은 글의 접근처럼 “관측 가능성 확보 → 원인 분리” 순서가 유효합니다.)
5) redirect-uri 템플릿을 명시적으로 고정하기
프록시 환경이 복잡하거나, 특정 도메인으로만 강제하고 싶다면 redirect-uri를 명시할 수 있습니다.
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope:
- openid
- profile
- email
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
위는 기본값과 같아 보이지만, 프로젝트에서 커스터마이징하다가 템플릿이 바뀌었는지 점검하는 기준점이 됩니다.
만약 운영 도메인이 고정이고 baseUrl 계산을 믿기 어렵다면(권장하진 않지만) 완전 고정도 가능합니다.
spring:
security:
oauth2:
client:
registration:
google:
redirect-uri: "https://app.example.com/login/oauth2/code/google"
단점:
- 로컬/스테이징에서 재사용이 어려움
- 도메인 변경 시 설정 누락 위험
그래서 보통은 Forwarded 헤더 처리로 baseUrl을 정상화하는 쪽이 유지보수에 유리합니다.
6) 리버스 프록시(Nginx) 앞단에서의 실전 포인트
Nginx를 직접 운영한다면 아래 헤더가 반드시 전달되도록 설정합니다.
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_pass http://spring_upstream;
}
그리고 Spring Boot에서 server.forward-headers-strategy를 켜면 redirect_uri가 외부 기준으로 생성됩니다.
7) 환경별 Callback URL 관리 전략
redirect_uri 문제는 “코드”보다 “환경 설정”에서 더 자주 납니다. 아래 전략을 추천합니다.
(1) Provider 콘솔에 환경별로 정확히 등록
http://localhost:8080/login/oauth2/code/googlehttps://stg.example.com/login/oauth2/code/googlehttps://app.example.com/login/oauth2/code/google
가능하면 와일드카드/정규식 허용 여부를 확인하고, 허용한다면 최소 범위로 사용합니다.
(2) 도메인/경로 정책을 단순화
- 운영은 무조건
https+ 단일 도메인 - 서브패스 배포를 피하거나, 피할 수 없다면 컨텍스트패스를 고정
(3) 배포 파이프라인에서 설정 누락 방지
GitHub Actions 등으로 배포할 때 환경변수 누락/오타로 다른 도메인이 들어가 redirect_uri가 틀어지는 경우가 있습니다. OIDC 기반 배포를 쓰면 시크릿 관리가 단순해져 이런 실수를 줄이기 좋습니다. 관련해서는 GitHub Actions OIDC로 AWS 키 없이 배포하기도 함께 참고할 만합니다.
8) 디버깅을 자동화: redirect_uri를 서버에서 재현해보기
장애 상황에서 “Spring이 계산한 baseUrl”을 코드로 찍어보면 빠릅니다. 예를 들어 임시로 컨트롤러를 만들어 현재 요청 정보를 확인합니다.
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class DebugController {
@GetMapping("/debug/request")
public Map<String, Object> debug(HttpServletRequest request) {
return Map.of(
"scheme", request.getScheme(),
"serverName", request.getServerName(),
"serverPort", request.getServerPort(),
"contextPath", request.getContextPath(),
"requestURL", request.getRequestURL().toString(),
"xForwardedProto", request.getHeader("X-Forwarded-Proto"),
"xForwardedHost", request.getHeader("X-Forwarded-Host"),
"xForwardedPort", request.getHeader("X-Forwarded-Port"),
"forwarded", request.getHeader("Forwarded")
);
}
}
- 여기서
scheme/serverPort가 외부와 다르면 Forwarded 헤더 처리가 안 된 것입니다. X-Forwarded-*가 아예 비어 있으면 프록시/Ingress 설정부터 확인해야 합니다.
9) 결론: 해결 순서(가장 빠른 루트)
- 브라우저에서 Provider authorize URL의
redirect_uri를 그대로 복사 - Provider 콘솔에 등록된 Callback URL과 문자 단위로 비교
- 운영에서만 문제면 Forwarded 헤더 처리부터 적용
server.forward-headers-strategy: framework- 프록시가
X-Forwarded-Proto/Host/Port를 넣는지 확인
- 컨텍스트패스/서브패스가 있으면 경로 정책을 단순화하거나 Provider 등록값을 동일하게
- 최후의 수단으로
redirect-uri를 완전 고정(환경별 분기 필수)
redirect_uri 불일치는 원인이 다양해 보여도, 결국 “Spring이 만든 redirect_uri”와 “Provider에 등록된 값”의 차이로 귀결됩니다. 관측(실제 값 확인) → 원인 분리(프록시/헤더/경로) → 설정 반영(Forwarded 처리) 순서로 접근하면 대부분 짧게 끝납니다.