지난 포스트에서 스프링 OAuth2 의존성 org.springframework.boot:spring-boot-starter-oauth2-client
을 추가하고 OAuth2 설정을 통해 소셜 로그인을 구현해보았습니다. 이번 포스트에서는 프론트엔드로부터 로그인 요청이 왔을 때 스프링 시큐리티에서 어떻게 처리하는지 코드를 통해 살펴보겠습니다.
이전 포스트에서 첨부했던 OAuth2 로그인 Flow 입니다. 이번 포스트에서는 이 과정이 실제 코드에서 어떻게 처리되는지 확인해보겠습니다.
위의 로그인 과정에서 최초 프론트엔드에서 백엔드에 로그인 주소를 요청하고 백엔드에서 로그인 페이지를 리턴하는 부분은 OAuth2AuthorizationRequestRedirectFilter 에서 처리됩니다.
아래는 스프링 시큐리티 코드 분석과 디버깅을 통해 그려본 시퀀스 다이어그램입니다.
OAuth2 로그인 Flow를 보면 사용자가 소셜 로그인 버튼 클릭시 프론트엔드에서 아래 URL로 백엔드에 요청합니다.
GET http://localhost:8080/oauth2/authorization/google?redirect_uri=http://localhost:3000&mode=login
백엔드에서 해당 요청이 들어오면 OAuth2AuthorizationRequestRedirectFilter 에서 요청을 처리합니다.
doFilterInternal
메서드가 호출됩니다. resolve
메서드를 호출합니다. 이 메서드에서는 요청 URL과 파라미터를 검사하여 특정 조건에 일치하면 OAuth2AuthorizationRequest 객체를 생성하여 리턴합니다.AuthorizationRequestRepository 의 saveAuthorizationRequest
메서드에서는 OAuth2AuthorizationRequest 객체를 저장합니다. 예제 프로젝트에서는 HttpCookieOAuth2AuthorizationRequestRepository 사용자 정의 클래스를 구현하여 쿠키에 OAuth2AuthorizationRequest 객체를 직렬화하여 저장하였습니다.
그 이후 OAuth2AuthorizationRequest 객체의 authorizationRequestUri
주소로 리다이렉트 합니다. 이 주소에는 각 OAuth2 제공자의 로그인 페이지 URL이 담겨있습니다.
resolve
메서드가 호출됩니다.resolve
메서드에서는 OAuth2AuthorizationRequest 객체를 생성하여 리턴합니다. 이 객체에는 리다이렉트 할 로그인 페이지 URL과 관련된 정보 들이 담겨있습니다./oauth2/authorization/{registrationId}
패턴과 일치하는지 확인하고 일치하면 registrationId
을 추출합니다. resolveRegistrationId
메서드에서 이 부분을 처리합니다.action
파라미터가 있는지 확인하고 있다면 추출합니다. 없다면 login
을 리턴합니다.application.properties
에 정의한 spring.security.oauth2.client.registration
값과 registrationId
, action
을 사용하여 OAuth2AuthorizationRequest 객체를 생성하여 리턴합니다.사용자가 로그인 페이지에서 로그인을 완료하였을 때 OAuth2 제공자 인증 서버에서 인가 코드가 사용자 서비스 백엔드로 리다이렉트 됩니다. 이 때 리다이렉트 요청은 OAuth2LoginAuthenticationFilter 에서 처리됩니다.
아래는 스프링 시큐리티 코드 분석과 디버깅을 통해 그려본 시퀀스 다이어그램입니다.
OAuth2 서비스 제공자가 사용자 서비스 서버로 리다이렉트하면 HTTP 요청은 스프링 시큐리티 필터 체인을 거쳐 OAuth2LoginAuthenticationFilter 로 전달됩니다. 이 때 doFilter 메서드는 OAuth2LoginAuthenticationFilter에 정의되어 있지 않아 상위 클래스의 메서드가 호출됩니다. 바로 AbstractAuthenticationProcessingFilter 입니다.
먼저 요청 URL이 /login/oauth2/code/*
패턴과 일치하는지 확인합니다. 이전 포스트에서 구글, 네이버, 카카오의 OAuth2 애플리케이션 설정에 Redirect URI를 http://localhost:8080/login/oauth2/code/google
과 같은 형식으로 설정했었습니다.
요청 URL이 패턴과 일치하면 인증을 시도하기 위해 attemptAuthentication
메서드를 호출합니다. 해당 메서드는 추상 메서드이고 하위 클래스인 OAuth2LoginAuthenticationFilter 에서 구현합니다. 디자인 패턴 중 하나인 템플릿 메서드 패턴입니다.
인증 과정이 성공적으로 끝나면 successfulAuthentication
메서드에서 스프링 시큐리티 컨텍스트에 인증 객체를 저장하고, successHandler 를 호출합니다.
this.getAuthenticationManager().authenticate(authenticationRequest)
코드에서 getAuthenticationManager()
가 ProviderManager
라는 객체를 리턴합니다. ProviderManager
객체는 AuthenticationProvider
인터페이스 구현체를 리스트로 가지고 있고 이 리스트를 순회하면서 인증을 시도하는 객체입니다.ProviderManager
객체는 AuthenticationProvider
인터페이스의 구현체들을 리스트로 가지고 인증 요청이 왔을 때 Provider
리스트를 순회하여 인증을 지원 하는 Provider
에게 인증을 위임합니다.OAuth2LoginAuthenticationFilter
에서 ProviderManager
호출시 OAuth2LoginAuthenticationToken
을 파라미터로 전달합니다. 따라서 OAuth2LoginAuthenticationProvider
의 supports
메서드가 조건을 만족하므로 OAuth2LoginAuthenticationProvider
의 authenticate
메서드가 호출됩니다.Provider
구현체가 리스트에 포함되어 있습니다.OAuth2LoginAuthenticationProvider
객체는 인가 코드로 OAuth2 제공자 인증 서버에서 액세스 토큰을 발급 받고, 액세스 토큰으로 사용자 정보를 받아 인증 객체를 생성하고 리턴하는 로직을 처리합니다. 이 때 액세스 토큰 발급과 사용자 정보를 받아오는 부분은 각각 OAuth2AuthorizationCodeAuthenticationProvider
객체와 UserService
객체에게 위임합니다.authorizationCodeAuthenticationProvider
참조 변수에 OAuth2AuthorizationCodeAuthenticationProvider
객체를 생성하여 저장합니다. 이 객체에서 인가 코드로 인증 서버에서 액세스 토큰을 받아오는 로직을 수행합니다.userService
참조 변수에 UserService
객체를 저장합니다. userService
참조 변수는 직접 작성한 코드인CustomOAuth2UserService
클래스의 객체가 저장됩니다. 이 객체에서 액세스 토큰으로 사용자 정보를 받아옵니다.DefaultAuthorizationCodeTokenResponseClient
객체에게 위임합니다.OAuth2AuthorizationCodeAuthenticationToken
객체로 감싸서 리턴합니다.RestTemplate
객체를 생성하고 exchange
메서드를 통해 HTTP 요청을 보냅니다.ClientAuthenticationMethodValidatingRequestEntityConverter
객체에게 위임하고 이 객체는 내부에서 OAuth2AuthorizationCodeGrantRequestEntityConverter
객체에게 위임합니다.OAuth2AuthorizationCodeGrantRequestEntityConverter
클래스의 상위 클래스입니다. convert
메서드에서 토큰 발급 요청의 헤더와 파라미터를 생성하고 요청 객체를 리턴합니다.OAuth2AuthorizationGrantRequestEntityUtils
객체에서 생성되고, 파라미터는 OAuth2AuthorizationCodeGrantRequestEntityConverter
객체에서 생성됩니다.client-authentication-method
필드의 값에 따라서 생성되는 헤더의 값이 달라집니다.client-authentication-method
필드의 값이 client_secret_basic
일 경우 헤더에 clientId
와 clientSecret
이 추가됩니다. 구글의 경우 client_secret_basic
을 사용합니다. 반면 client_secret_post
일 경우 파라미터에 clientId
와 clientSecret
이 추가됩니다. 이는 밑에서 알아보겠습니다.client-authentication-method
필드 값이 client_secret_basic
이 아니면 파라미터에 clientId
를 추가합니다.client-authentication-method
필드 값이 client_secret_post
이면 파라미터에 clientSecret
을 추가합니다.client-authentication-method
필드 값이 client_secret_post
이면 파라미터에 clientId
와 clientSecret
이 추가됩니다.OAuth2LoginAuthenticationProvider
에서 발급 받은 액세스 토큰을 이 객체에게 전달합니다. 따라서 이 객체에서 액세스 토큰으로 OAuth2 제공자 리소스 서버에서 사용자 정보를 얻어오고 정보를 가지고 OAuth2User
인터페이스 구현체인 인증 객체를 생성하여 리턴해야합니다.DefaultOAuth2UserService
객체에게 위임합니다.OAuth2User
구현체로 리턴하는 역할을 수행합니다.OAuth2UserRequestEntityConverter
객체에게 위임합니다.spring.security.oauth2.client.registration.provider.{registrationId}.user-info-authentication-method
값에 따라 액세스 토큰이 포함되는 위치가 달라집니다. header
, form
, query
값이 있으며 기본 값은 header
입니다.user-info-authentication-method
값이 form
일 경우 POST로 요청하고, 그 외의 값일 경우 GET으로 요청합니다.CustomOAuth2UserService
에서 OAuth2User
구현체를 리턴하면, OAuth2LoginAuthenticationProvider
에서 OAUth2LoginAuthenticationToken
객체로 감싸서 리턴합니다. 이를 OAuth2LoginAuthenticationFilter
에서 받아서 OAuth2AuthenticationToken
객체로 변환하여 리턴합니다. 상위 클래스인 AbstractAuthenticationProcessingFilter에 정의된 doFilter
메서드에서 받아서 successfulAuthentication
메서드를 호출하게 됩니다.successfulAuthentication
메서드에서는 시큐리티 컨텍스트에 인증 객체를 저장합니다. 따라서 다음 필터 부터는 인증 정보를 시큐리티 컨텍스트를 통해 전역적으로 사용할 수 있습니다.successHandler
의 onAuthenticationSuccess
메서드를 호출합니다.
감사합니다. 혹시 이 게시글에 대한 코드는 없을까요 ?