개요
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 |