이전글
스프링 시큐리티는 OAuth 로그인 요청을 어떤식으로 처리할까?
스프링 시큐리티는 OAuth로그인 관련해서 크게 2가지 작업을 처리한다.
1) set up
2) OAuth 로그인 요청 처리
우리는 스프링 시큐리티를 사용할 때, 단지 application.properties나 application.yml에 client-id, client-secret 등의 정보만 적어줬다. 신기하게도 설정 파일(.yml or .properties)에 적어준 정보를 가지고 로그인이 잘 진행되었었다. 어떻게 된 일일까?
스프링 시큐리티는 설정 파일에 적어준 정보들로 애플리케이션 실행 시 OAuth2ClientProperties
객체를 생성한다.
우리가 정보를 적어줬던 application-oauth.yml
과OAuth2ClientProperties
를 비교해보자.
딱 봐도 비슷한 구조인 걸 볼 수 있다. 우리가 설정 파일에 적어준 정보들이 객체로 만들어져 사용되는 것이다.
@ConfigurationProperties
어노테이션으로 우리가 설정 파일에 적어준 정보들을 OAuth2ClientProperties
필드에 주입하여 객체를 만들어준다.
*참고 @ConfigurationProperties란?
위의 설정 파일로 예시를 들자면, OAuth2ClientProperties
의 필드인 provider와 registration의 key값으로는 github, google, naver가 들어가고, 각각 value값으로 Provider 객체, Registration 객체가 들어간다. Registration 객체의 필드에 client-id, client-secret등의 값이 들어가 있는 것이다.
@ConfigurationProperties
를 사용하려면 @EnableConfigurationProperties
어노테이션을 사용하여 설정 파일의 데이터를 바인딩 할 클래스를 값으로 주면 된다. spring security에는 OAuth2ClientRegistrationRepositoryConfiguration
에 아래와 같이 설정되어있다.
참고로 아래의 의존성을 넣어줘야 정상 동작한다.
implementation 'org.springframework.boot:spring-boot-configuration-processor'
spring security에서는 애플리케이션 실행시 위의 설정을 통해 OAuth2ClientProperties
를 빈으로 등록하고, OAuth2ClientProperties
내부 값들을 통해 각 OAuth2 서버 별로 ClientRegistration
라는 객체들을 만들어 각각 InMemoryRepository에 저장한다. 이런 과정이 바로 OAuth2ClientPropertiesRegistrationAdapter
에서 이루어진다.
좀 더 자세히 살펴보자.
OAuth2ClientPropertiesRegistrationAdapter
의 getClientRegistrations()
메서드를 살펴보면, 아까 빈으로 등록된 OAuth2ClientProperties
에서 Registration
객체를 가져와 ClientRegistration
객체(clientId, clientSecret 등의 정보를 가지고 있음)를 만드는 것을 볼 수 있다. 여기서 key값이 github, google, naver인 것이다.
실제로 ClientRegistration
을 만드는 과정은 바로 밑의 getClientRegistration()
메서드에서 일어난다.
이 코드를 분석하다보면 내부적으로 OAuth2ClientPropertiesRegistrationAdapter
의 getCommonProvider()
라는 메서드가 호출된다. 이 메서드에서 providerId(ex. github, google, naver)를 통해 CommonOAuth2Provider
객체를 얻어온다.
CommonOAuth2Provider
는 자주 사용하는 OAuth2 server의 정보들을 미리 세팅해놓은 enum 클래스이다. 우리가 spring security를 사용해서 OAuth2 로그인 구현을 했을때, application.yml에 github, google과 달리 naver에는 많은 정보들을 적어준 이유가, CommonOAuth2Provider
에 naver가 등록되어 있지 않기 때문이다.
이런 과정을 거쳐 application.yml에 적어준 OAuth2 서버마다 각각 ClientRegistration
객체가 생성되고 이 ClientRegistration
객체들을 Map에 담아 InMemoryClientRegistrationRepository
에 저장한다. InMemoryClientRegistrationRepository
는 말그대로 memory 저장소이다. 필드로 Map을 가진다.
이렇게 각 OAuth2 server의 이름을 key로 가지고 각 OAuth2 server의 ClientRegistration
객체를 value로 가지는 InMemoryClientRegistrationRepository
가 컴포넌트 스캔에 의해 빈으로 등록되고, 우리는 로그인 요청 시 InMemoryClientRegistrationRepository
에서 ClientRegistration
를 가져와 사용하는 것이다.
즉, 우리가 스프링 애플리케이션을 실행할 때 크게보면 아래의 작업이 일어난다.
OAuth2ClientProperties
생성OAuth2ClientPropertiesRegistrationAdapter
를 통해 OAuth2ClientProperties
에서 각 OAuth2 server 마다 ClientRegistration
생성ClientRegistration
리스트를 InMemoryClientRegistrationRepository
에 저장spring security는 실제로 로그인 요청이 오면 어떻게 처리할까?
spring security는 dispatcher servlet에 요청이 들어가기 전 거쳐야하는 여러 필터를 제공한다. spring security는 필터 체인을 제공한다. 요청이 들어오면 미리 등록되어 있는 필터들을 연쇄적으로 모두 거친 후 dispatcher servlet에 요청이 도달하게 된다.
그 중 OAuth2AuthorizationRequestRedirectFilter
에서 /oauth2/authorization/{registrationId}
로 온 요청을 처리한다. 이때 OAuth2AuthorizationRequestRedirectFilter
의 내부에서 OAuth2AuthorizationRequestResolver
를 통해 OAuth2 로그인 요청을 처리한다.
OAuth2AuthorizationRequestRedirectFilter
는 OAuth2AuthorizationRequestResolver
의 resolve()
메서드를 호출한다. /oauth2/authorization/{registrationId}
로 온 요청에서 registrationId를 추출한다.(ex. google, github, naver)
추출한 registrationId를 통해 InMemoryClientRegistrationRepository
의 findByRegistrationId()
메서드를 통해 set up 과정에서 만들어진 ClientRegistration
객체를 가져온다. 그 후, OAuth2AuthorizationRequest
객체를 만든다.
OAuth2AuthorizationRequestRedirectFilter
에서는 얻어온 OAuth2AuthorizationRequest
객체의 authorizationRequestUri
로 redirect 요청을 보낸다.
그러면 우리의 아이디, 비밀번호를 칠 수 있는 창으로 리다이렉트 된다.
아이디 / 비밀번호를 입력하고 나서 발생하는 요청은 OAuth2LoginAuthenticationFilter
에서 처리한다.
registrationId를 통해 ClientRegistration
객체를 가져온다.
그 후, OAuth2 server의 access token을 얻기 위한 authorization code(아이디 / 비밀번호 입력시 생성됨. HttpServletRequest에 이미 담겨있는 정보)를 OAuth2AuthorizationResponse
객체로 만들고, OAuth2AuthorizationResponse
를 통해 access token을 가져온다.
OAuth2 server에서 access token을 가져올 때, ProviderManager
의 authenticate()
메서드가 이용된다. 여러 provider 중 OAuth2LoginAuthenticationProvider
가 이 과정을 수행한다.
OAuth2LoginAuthenticationProvider
의 authenticate()
메서드는 내부적으로 DefaultAuthorizationCodeTokenResponseClient
의 getTokenResponse()
메서드를 호출하고,
DefaultAuthorizationCodeTokenResponseClient
의 getTokenResponse()
메서드에서 RestTemplate
을 이용해 OAuth2 server로 access token request를 보낸다.
access token을 얻었으면, 이 access token을 이용해서 OAuth2 server에서 유저 정보를 가져올 수 있다. 이 과정은 OAuth2LoginAuthenticationProvider
의 authenticate()
메서드 내부에서 access token을 얻은 다음 바로 수행된다. userService의 loadUser()
메서드를 통해 유저 정보를 얻어온다.
이때, userService는 security config에서 설정해준 userService이다.(참고 [OAuth + Spring Boot + JWT] 2. 스프링 시큐리티로 OAuth 구현해보기)
나같은 경우는 custom service를 만들어 주었기 때문에 이전 글에서 작성했던 OAuthService
의 loadUser()
가 실행된다. loadUser()
내부의 delegate.loadUser()
가 내부적으로 OAuth2 server로 유저 정보를 요청한다. 이때 delegate는 DefaultOAuth2UserService
이다.
DefaultOAuth2UserService
의 loadUser()
에서는 access token을 얻을 때와 같이 RestTemplate
을 이용해서 OAuth2 server에 요청을 보낸다.
결국 OAuth 로그인 요청이 들어오면 spring security는 아래와 같은 과정을 수행한다.
1. /oauth2/authorization/{registrationId}
로 요청이 오면 OAuth2AuthorizationRequestRedirectFilter
에서 registrationId에 따라 아이디 / 비밀번호를 입력할 수 있는 uri로 리다이렉트 시킨다.
2. 아이디 / 비밀번호를 입력 후 얻을 수 있는 authorization code로 OAuth2LoginAuthenticationFilter
에서 OAuth2 server와 소통한다.
2-1. OAuth2LoginAuthenticationProvider
를 통해 access token을 얻어온다.
2-2. OAuth2LoginAuthenticationProvider
를 통해 access token을 이용해서 유저 정보를 얻어온다.
OAuth2ClientProperties
생성 한다.OAuth2ClientPropertiesRegistrationAdapter
를 통해 OAuth2ClientProperties
에서 각 OAuth2 server 마다 ClientRegistration
생성 한다.ClientRegistration
리스트를 InMemoryClientRegistrationRepository
에 저장 한다./oauth2/authorization/{registrationId}
로 OAuth2 로그인 요청을 한다.OAuth2AuthorizationRequestRedirectFilter
에서 registrationId에 따라 아이디 / 비밀번호를 입력할 수 있는 uri로 리다이렉트 시킨다.OAuth2LoginAuthenticationFilter
에서 OAuth2 server와 소통한다.OAuth2LoginAuthenticationProvider
를 통해 access token을 얻어온다.OAuth2LoginAuthenticationProvider
를 통해 access token을 이용해서 유저 정보를 얻어온다. 디버깅을 해보면서 파악해봤지만, 모든 코드를 이해하지는 못했다.(틀린 부분도 있을 수도..)
다음 포스팅에서는 이번에 이해한 spring security의 구조를 참고해 필요한 부분만 가져와 spring security 없이 OAuth2 로그인을 구현할 것이다.
잘 보고 갑니다