SpringSecurity에서 OAuth2 로그인 시 쿼리 파라미터 유지하기

2025. 4. 28. 16:41·Web-Spring

개요

이 문서는 SpringSecurity와 Authorization Code Grant 방식에서의 OAuth2 Client를 통한 소셜로그인이 구축된 환경에서 소셜로그인 요청시에 포함된 쿼리 파라미터를 최종 리다이렉트 주소까지 유지하는 방법을 알려줘요
이 기능으로 OAuth2 Client 의 최종 리다이렉트 주소를 동적으로 지정하는 등의 다양한 활용이 가능해요

Authorization Code Grant 개념

OAuth2에서는 여러 인증방식이 있고, 그 중에서 kakao와 Google등의 소셜로그인 제공자에게 인가코드(Authorization Code)를 전달하는 방식을 Authorization Code Grant 이라고 해요

Authorization Code Grant 기본 동작

Authorization Code Grant 방식은 소셜로그인 제공자가 로그인 폼을 제시하고

 

사용자가 로그인에 성공하면 설정해둔 리다이렉트 주소지로 인가코드(Authorization Code)를 함께 보내요

 

해당 인가코드(Authorization Code)를 가지고 소셜로그인 제공자에게 여러 유저정보 및 AccessToken 을 얻어요.

SpringSecurity와 OAuth2 Client 로 구현하는 Authorizaion Code Grant

Springboot 에서 SpringSecurity 와 OAuth2를 통해 소셜로그인을 개발하였을 때 AuthenticationSuccessHandler의 구현체를 만들어서 onAuthenticationSuccess 메소드를 재정의 해요

 

왜냐하면 onAuthenticationSuccess 메소드는 Server가 인가코드(Authorization Code)를 받고서 사용자의 정보를 조회하는데 성공했을 때 호출하기 때문입니다.

 

onAuthenticationSuccess 메소드의 역할은 소셜로그인을 처리 완료하고 사용자의 인증정보가 담긴 토큰을 발행해요

 

발행된 토큰을 가지고 사용자가 머무르던 주소로 이동시키기 위해서 HttpServletResponse 의 sendRedirect() 를 사용해요.

 

사전 요구사항

서버 환경

JDK 21
Springboot 3.4.2-SNAPSHOT
 

dependencies

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
 

OAuth2 Client 의 Authrozation Code Grant 동작을 이해하고, 로그인이 구축된 상태여야 합니다.

 

AuthenticationSuccessHandler의 구현체가 필요합니다.

문제 상황

프론트엔드 개발자들은 로컬에서 구현한 기능을 localhost에서 태스트하곤 합니다.

 

필요하다면 localhost에서 개발용 서버로 소셜로그인 요청을 보내기도 해요

 

여기서 OAuth2 Client는 변수 혹은 프로퍼티 등으로 정의된 한가지 호스트로만 리다이렉트 요청을 보냅니다.

 

이 상황에서 쿼리파라미터를 활용하여 동적으로 리다이렉트 호스트를 처리하려고 시도합니다.

 
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {

  @Value("${front.dev.redirect-uri}")
  private String devRedirectUri; // 프론트 개발용 주소

  @Value("${front.local.redirect-uri}")
  private String localRedirectUri; // 프론트 로컬 주소

  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

          // ... 각종 동작들

          if (request.getParameter("local") != null && request.getParameter("local").equals("true")) {
              final String value = request.getParameter("local");
              if (value.equals("true")) {
                  response.sendRedirect(localRedirectUri);
                  return;
          }

          response.sendRedirect(devRedirectUri);
  }
}

onAuthenticationSuccess는 최종 소셜로그인 동작을 완료한 후에 동작하는 메소드에요

 

이 메소드에서 주로  HttpServletResponse의 sendRedirect를 활용하여 사용자의 화면이 프론트엔드가 구현한 UI로 돌아가도록 구현합니다.

 

위 코드는 HttpServletRequest의 getParameter 메소드를 통해 쿼리파라미에 local=true가 포함되어 있다면 localhost로 리다이렉트 하는것을 목표로 합니다.

 

실제로 요청 url에?local=true를 붙여서 실행시키면 localRedirectUri로 보낼 것이라고 예상하지만 devRedirectUri 로 보내버립니다.

원인

Authorization Code Grant 동작 방식을 생각해보면 알 수 있어요

먼저 소셜 로그인 폼을 요청할 때, 소셜로그인 제공자는 자신들의 폼으로 리다이렉트 시켜요.

HTTP/1.1 302 Found
Location: https://accounts.google.com/o/oauth2/v2/auth?client_id=xxx&scope=xxx

 

이런식으로 리다이렉트 시키는 담당이 OAuth2AuthorizationRequestRedirectFilter와 DefaultOAuth2AuthorizationRequestResolver 입니다.

public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {

    public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";

    // 핵심 코드는 아래에

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
            if (authorizationRequest != null) {
                this.sendRedirectForAuthorization(request, response, authorizationRequest);
                return;
            }
        }
        catch (Exception ex) {
            AuthenticationException wrappedException = new OAuth2AuthorizationRequestException(ex);
            this.authenticationFailureHandler.onAuthenticationFailure(request, response, wrappedException);
            return;
        }
        // 많은 코드블럭들...
    }

    private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
            OAuth2AuthorizationRequest authorizationRequest) throws IOException {
        if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
            this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
        }
        this.authorizationRedirectStrategy.sendRedirect(request, response,
                authorizationRequest.getAuthorizationRequestUri());
    }
}
public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {

    public DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository,
            String authorizationRequestBaseUri) {
        Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
        Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty");
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.authorizationRequestMatcher = new AntPathRequestMatcher(
                authorizationRequestBaseUri + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}");
    }

    // 많은 코드들...

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        String registrationId = resolveRegistrationId(request);
        if (registrationId == null) {
            return null;
        }
        String redirectUriAction = getAction(request, "login");
        return resolve(request, registrationId, redirectUriAction);
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) {
        if (registrationId == null) {
            return null;
        }
        String redirectUriAction = getAction(request, "authorize");
        return resolve(request, registrationId, redirectUriAction);
    }

    // 많은 코드들 ...

    private String resolveRegistrationId(HttpServletRequest request) {
        if (this.authorizationRequestMatcher.matches(request)) {
            return this.authorizationRequestMatcher.matcher(request)
                .getVariables()
                .get(REGISTRATION_ID_URI_VARIABLE_NAME);
        }
        return null;
    }

 

코드의 동작을 간략히 요약하면 OAuth2AuthorizationRequestRedirectFilter는 OAuth2AuthorizationRequestResolver 타입의 구현체인 DefaultOAuth2AuthorizationRequestResolver를 사용하여 /oauth2/authorization/{소셜로그인 제공자} 의 패턴을 가지는 URL 인지 매칭시키고

매칭된다면 소셜 로그인 폼을 요청하는 URL 을 만들어서 리다이렉트를 시켜요.

 

여기서 /oauth2/authorization/{소셜로그인 제공자} 패턴은 일반적으로 소셜로그인 폼 요청에 사용되는 OAuth2 Client가 제공하는 Default URL 입니다.

 

이 과정에서 어떤 쿼리파라미터를 보내든, OAuth2AuthorizationRequestRedirectFilter 의 정해진 소셜 로그인 폼 URL로 리다이렉트를 킵니다.

 

그래서 프론트엔드가 서버로 소셜로그인 폼 요청에 대한 URL에 쿼리파라미터를 붙이더라도 유실되요.

해결 방법

동작의 목표는 OAuth2AuthorizationRequestRedirectFilter 에서 DefaultOAuth2AuthorizationRequestResolver를 사용하여 소셜 로그인 폼을 만들되, 쿼리파라미터(?local=true)가 존재한다면 붙여주도록 만들거에요

 

다행히 SecurityFilterChain을 만들 때 OAuth2AuthorizationRequestResolver구현체를 DefaultOAuth2AuthorizationRequestResolver가 아닌 별도의 구현체를 사용할 수 있게 되어있습니다.

public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) {

        return http
                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
                .csrf(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((authorizeReq) -> {
                    // 내부코드 ...
                })
                .oauth2Login(oauth2 ->
                    oauth2.authorizationEndpoint(authorization -> authorization.authorizationRequestResolver(// 바로여기!))
                    // 내부 코드 ...
                )

    }
}

 

따라서 OAuth2AuthorizationRequestResolver를 커스텀한 CustomOAuth2AuthorizationRequestResolver 를 만들어서 요청 URL에 쿼리 파라미터가 있다면 DefaultOAuth2AuthorizationRequestResolver의 동작 결과 URL에 덧붙이도록 구현합니다.

public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {

	private final DefaultOAuth2AuthorizationRequestResolver defaultResolver;

	public CustomOAuth2AuthorizationRequestResolver(DefaultOAuth2AuthorizationRequestResolver defaultResolver) {

		this.defaultResolver = defaultResolver;
	}

	@Override
	public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
		OAuth2AuthorizationRequest authorizationRequest = defaultResolver.resolve(request);
		return modifyAuthorizationRequest(authorizationRequest, request);
	}

	@Override
	public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
		OAuth2AuthorizationRequest authorizationRequest = defaultResolver.resolve(request, clientRegistrationId);
		return modifyAuthorizationRequest(authorizationRequest, request);
	}

	private OAuth2AuthorizationRequest modifyAuthorizationRequest(OAuth2AuthorizationRequest original, HttpServletRequest request) {

		if (original == null) {
			return null;
		}
		String redirectUri = original.getRedirectUri();

		Map<String, String[]> requestParams = request.getParameterMap();

		MultiValueMap<String, String> multiValueMap = MultiValueMap.fromMultiValue(
			requestParams.entrySet().stream()
				.collect(Collectors.toMap(
					Map.Entry::getKey,
					e -> Arrays.asList(e.getValue())
				)
			)
		);

		redirectUri = UriComponentsBuilder.fromUriString(redirectUri)
			.queryParams(multiValueMap)
			.toUriString();

		return OAuth2AuthorizationRequest.from(original)
			.redirectUri(redirectUri)
			.additionalParameters(original.getAdditionalParameters())
			.build();
	}
}

 

기존에 DefaultOAuth2AuthorizationRequestResolver 가 만든 쿼리파라미터에 파라미터를 추가하는 방식으로 구현해야 하기에 기존 쿼리파라미터를 OAuth2AuthorizationRequest에서 가져와서 더해요

 

이렇게 구현하면 Authorization Code Grant 방식에서 최종 OAuth2SuccessHandler에서 까지 전달 될 거에요.

 

 

'Web-Spring' 카테고리의 다른 글

Springboot에서 SpringSecurity는 어떻게 초기화되는가? - 중  (1) 2025.05.17
Springboot에서 SpringSecurity는 어떻게 초기화되는가? - 상  (0) 2025.05.03
쿠키와 SameSite, Secure, HttpOnly  (0) 2025.04.17
@JsonTypeInfo 를 통해 유연하게 Json과 Java객체 매핑하기  (0) 2025.04.14
JUnit  (2) 2023.12.12
'Web-Spring' 카테고리의 다른 글
  • Springboot에서 SpringSecurity는 어떻게 초기화되는가? - 중
  • Springboot에서 SpringSecurity는 어떻게 초기화되는가? - 상
  • 쿠키와 SameSite, Secure, HttpOnly
  • @JsonTypeInfo 를 통해 유연하게 Json과 Java객체 매핑하기
devKhc
devKhc
  • devKhc
    개발저장소 by 회창
    devKhc
  • 전체
    오늘
    어제
    • 분류 전체보기 (24)
      • Web-Spring (7)
      • CS (5)
      • Infra (3)
      • DB (1)
      • Java (3)
      • CodeTree (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    인터럽트
    이중모드와 다중모드
    samesite=none
    Multi-Processor
    모드비트
    코드트리조별과제
    SpringBoot
    defaultoauth2authorizationrequestresolver
    운영체제
    JsonSubTypes
    set-cookie
    try with resources
    다중 코어 시스템
    코드트리
    springboot #jenkins #CI/CD #Docker #EC2
    코딩테스트
    인터럽트 #운영체제 #공룡책
    Nginx #proxy #리버스 프록시
    JsonTypeInfo
    Springsecurity
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
devKhc
SpringSecurity에서 OAuth2 로그인 시 쿼리 파라미터 유지하기
상단으로

티스토리툴바