[OAuth + Spring Boot + JWT] 3. 스프링 시큐리티는 OAuth 로그인을 어떻게 처리할까? 스프링 시큐리티 구조

Junseo Kim·2021년 8월 17일
15
post-thumbnail

이전글


스프링 시큐리티는 OAuth 로그인 요청을 어떤식으로 처리할까?

스프링 시큐리티는 OAuth로그인 관련해서 크게 2가지 작업을 처리한다.

1) set up
2) OAuth 로그인 요청 처리

🪨 Set up

프로퍼티 객체 생성 - OAuth2ClientProperties

우리는 스프링 시큐리티를 사용할 때, 단지 application.properties나 application.yml에 client-id, client-secret 등의 정보만 적어줬다. 신기하게도 설정 파일(.yml or .properties)에 적어준 정보를 가지고 로그인이 잘 진행되었었다. 어떻게 된 일일까?

스프링 시큐리티는 설정 파일에 적어준 정보들로 애플리케이션 실행 시 OAuth2ClientProperties 객체를 생성한다.

우리가 정보를 적어줬던 application-oauth.ymlOAuth2ClientProperties를 비교해보자.

딱 봐도 비슷한 구조인 걸 볼 수 있다. 우리가 설정 파일에 적어준 정보들이 객체로 만들어져 사용되는 것이다.

@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'

InMemoryRepository

spring security에서는 애플리케이션 실행시 위의 설정을 통해 OAuth2ClientProperties를 빈으로 등록하고, OAuth2ClientProperties 내부 값들을 통해 각 OAuth2 서버 별로 ClientRegistration라는 객체들을 만들어 각각 InMemoryRepository에 저장한다. 이런 과정이 바로 OAuth2ClientPropertiesRegistrationAdapter에서 이루어진다.

좀 더 자세히 살펴보자.

OAuth2ClientPropertiesRegistrationAdaptergetClientRegistrations() 메서드를 살펴보면, 아까 빈으로 등록된 OAuth2ClientProperties에서 Registration 객체를 가져와 ClientRegistration 객체(clientId, clientSecret 등의 정보를 가지고 있음)를 만드는 것을 볼 수 있다. 여기서 key값이 github, google, naver인 것이다.

실제로 ClientRegistration을 만드는 과정은 바로 밑의 getClientRegistration() 메서드에서 일어난다.

이 코드를 분석하다보면 내부적으로 OAuth2ClientPropertiesRegistrationAdaptergetCommonProvider()라는 메서드가 호출된다. 이 메서드에서 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를 가져와 사용하는 것이다.

📝 set up 정리

즉, 우리가 스프링 애플리케이션을 실행할 때 크게보면 아래의 작업이 일어난다.

  1. application.yml파일을 읽어 OAuth2ClientProperties 생성
  2. OAuth2ClientPropertiesRegistrationAdapter를 통해 OAuth2ClientProperties에서 각 OAuth2 server 마다 ClientRegistration 생성
  3. ClientRegistration 리스트를 InMemoryClientRegistrationRepository에 저장

⚙️ OAuth 로그인 요청 처리

spring security는 실제로 로그인 요청이 오면 어떻게 처리할까?

OAuth2AuthorizationRequestRedirectFilter

spring security는 dispatcher servlet에 요청이 들어가기 전 거쳐야하는 여러 필터를 제공한다. spring security는 필터 체인을 제공한다. 요청이 들어오면 미리 등록되어 있는 필터들을 연쇄적으로 모두 거친 후 dispatcher servlet에 요청이 도달하게 된다.

그 중 OAuth2AuthorizationRequestRedirectFilter에서 /oauth2/authorization/{registrationId}로 온 요청을 처리한다. 이때 OAuth2AuthorizationRequestRedirectFilter의 내부에서 OAuth2AuthorizationRequestResolver를 통해 OAuth2 로그인 요청을 처리한다.

OAuth2AuthorizationRequestRedirectFilterOAuth2AuthorizationRequestResolverresolve()메서드를 호출한다. /oauth2/authorization/{registrationId}로 온 요청에서 registrationId를 추출한다.(ex. google, github, naver)

추출한 registrationId를 통해 InMemoryClientRegistrationRepositoryfindByRegistrationId()메서드를 통해 set up 과정에서 만들어진 ClientRegistration 객체를 가져온다. 그 후, OAuth2AuthorizationRequest 객체를 만든다.

OAuth2AuthorizationRequestRedirectFilter에서는 얻어온 OAuth2AuthorizationRequest 객체의 authorizationRequestUri 로 redirect 요청을 보낸다.

그러면 우리의 아이디, 비밀번호를 칠 수 있는 창으로 리다이렉트 된다.

OAuth2LoginAuthenticationFilter

아이디 / 비밀번호를 입력하고 나서 발생하는 요청은 OAuth2LoginAuthenticationFilter에서 처리한다.

registrationId를 통해 ClientRegistration 객체를 가져온다.

그 후, OAuth2 server의 access token을 얻기 위한 authorization code(아이디 / 비밀번호 입력시 생성됨. HttpServletRequest에 이미 담겨있는 정보)를 OAuth2AuthorizationResponse 객체로 만들고, OAuth2AuthorizationResponse를 통해 access token을 가져온다.

access token 가져오기

OAuth2 server에서 access token을 가져올 때, ProviderManagerauthenticate()메서드가 이용된다. 여러 provider 중 OAuth2LoginAuthenticationProvider가 이 과정을 수행한다.

OAuth2LoginAuthenticationProviderauthenticate()메서드는 내부적으로 DefaultAuthorizationCodeTokenResponseClientgetTokenResponse() 메서드를 호출하고,

DefaultAuthorizationCodeTokenResponseClientgetTokenResponse() 메서드에서 RestTemplate을 이용해 OAuth2 server로 access token request를 보낸다.

유저 정보 가져오기

access token을 얻었으면, 이 access token을 이용해서 OAuth2 server에서 유저 정보를 가져올 수 있다. 이 과정은 OAuth2LoginAuthenticationProviderauthenticate()메서드 내부에서 access token을 얻은 다음 바로 수행된다. userService의 loadUser() 메서드를 통해 유저 정보를 얻어온다.

이때, userService는 security config에서 설정해준 userService이다.(참고 [OAuth + Spring Boot + JWT] 2. 스프링 시큐리티로 OAuth 구현해보기)

나같은 경우는 custom service를 만들어 주었기 때문에 이전 글에서 작성했던 OAuthServiceloadUser()가 실행된다. loadUser() 내부의 delegate.loadUser()가 내부적으로 OAuth2 server로 유저 정보를 요청한다. 이때 delegate는 DefaultOAuth2UserService이다.

DefaultOAuth2UserServiceloadUser()에서는 access token을 얻을 때와 같이 RestTemplate을 이용해서 OAuth2 server에 요청을 보낸다.

📝 OAuth 로그인 요청 처리 정리

결국 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을 이용해서 유저 정보를 얻어온다.

📝 최종 정리

  1. 애플리케이션을 실행한다.
  2. application.yml파일을 읽어 OAuth2ClientProperties 생성 한다.
  3. OAuth2ClientPropertiesRegistrationAdapter를 통해 OAuth2ClientProperties에서 각 OAuth2 server 마다 ClientRegistration 생성 한다.
  4. ClientRegistration 리스트를 InMemoryClientRegistrationRepository에 저장 한다.
  5. /oauth2/authorization/{registrationId}로 OAuth2 로그인 요청을 한다.
  6. 요청이 오면 OAuth2AuthorizationRequestRedirectFilter에서 registrationId에 따라 아이디 / 비밀번호를 입력할 수 있는 uri로 리다이렉트 시킨다.
  7. 아이디 / 비밀번호를 입력 후 얻을 수 있는 authorization code로 OAuth2LoginAuthenticationFilter에서 OAuth2 server와 소통한다.
    7-1. OAuth2LoginAuthenticationProvider를 통해 access token을 얻어온다.
    7-2. OAuth2LoginAuthenticationProvider를 통해 access token을 이용해서 유저 정보를 얻어온다.

🚗 Next

디버깅을 해보면서 파악해봤지만, 모든 코드를 이해하지는 못했다.(틀린 부분도 있을 수도..)
다음 포스팅에서는 이번에 이해한 spring security의 구조를 참고해 필요한 부분만 가져와 spring security 없이 OAuth2 로그인을 구현할 것이다.

2개의 댓글

comment-user-thumbnail
2022년 10월 16일

잘 보고 갑니다

답글 달기
comment-user-thumbnail
2023년 2월 13일

상세, 정확, 전달력.. 대박입니다. 얼마나 정확하게 아시는지 글을 통해 다 느껴지네요!

답글 달기