스프링 시큐리티 OAuth2 동작 원리

jjunhwan.kim·2023년 8월 29일
17

스프링

목록 보기
2/10
post-thumbnail

개요

지난 포스트에서 스프링 OAuth2 의존성 org.springframework.boot:spring-boot-starter-oauth2-client 을 추가하고 OAuth2 설정을 통해 소셜 로그인을 구현해보았습니다. 이번 포스트에서는 프론트엔드로부터 로그인 요청이 왔을 때 스프링 시큐리티에서 어떻게 처리하는지 코드를 통해 살펴보겠습니다.

스프링 시큐리티 필터 구조

  • 스프링 시큐리티와 스프링 OAuth2 Client 의존성을 추가하고 OAuth2 로그인을 설정하면 아래와 같이 필터 체인이 추가됩니다.
  • HTTP 요청이 들어오면 필터 -> 디스패처 서블릿 -> 인터셉터 -> 컨트롤러 순서로 요청이 전달되며 처리됩니다.
  • OAuth2 로그인 과정은 OAuth2AuthorizationRequestRedirectFilter 와 OAuth2LoginAuthenticationFilter 에서 처리됩니다.

OAuth2 로그인 Flow

이전 포스트에서 첨부했던 OAuth2 로그인 Flow 입니다. 이번 포스트에서는 이 과정이 실제 코드에서 어떻게 처리되는지 확인해보겠습니다.

OAuth2AuthorizationRequestRedirectFilter

위의 로그인 과정에서 최초 프론트엔드에서 백엔드에 로그인 주소를 요청하고 백엔드에서 로그인 페이지를 리턴하는 부분은 OAuth2AuthorizationRequestRedirectFilter 에서 처리됩니다.

아래는 스프링 시큐리티 코드 분석과 디버깅을 통해 그려본 시퀀스 다이어그램입니다.

OAuth2 로그인 Flow를 보면 사용자가 소셜 로그인 버튼 클릭시 프론트엔드에서 아래 URL로 백엔드에 요청합니다.

GET http://localhost:8080/oauth2/authorization/google?redirect_uri=http://localhost:3000&mode=login

백엔드에서 해당 요청이 들어오면 OAuth2AuthorizationRequestRedirectFilter 에서 요청을 처리합니다.

OAuth2AuthorizationRequestRedirectFilter

  • OAuth2AuthorizationRequestRedirectFilter 의 doFilterInternal 메서드가 호출됩니다.
  • 메서드 안에서 OAuth2AuthorizationRequestResolver 객체의 resolve 메서드를 호출합니다. 이 메서드에서는 요청 URL과 파라미터를 검사하여 특정 조건에 일치하면 OAuth2AuthorizationRequest 객체를 생성하여 리턴합니다.

  • AuthorizationRequestRepository 의 saveAuthorizationRequest 메서드에서는 OAuth2AuthorizationRequest 객체를 저장합니다. 예제 프로젝트에서는 HttpCookieOAuth2AuthorizationRequestRepository 사용자 정의 클래스를 구현하여 쿠키에 OAuth2AuthorizationRequest 객체를 직렬화하여 저장하였습니다.

  • 그 이후 OAuth2AuthorizationRequest 객체의 authorizationRequestUri 주소로 리다이렉트 합니다. 이 주소에는 각 OAuth2 제공자의 로그인 페이지 URL이 담겨있습니다.

DefaultOAuth2AuthorizationRequestResolver

  • 위에서 OAuth2AuthorizationRequestResolver는 인터페이스이고 실제 구현체는 DefaultOAuth2AuthorizationRequestResolver 입니다.
  • DefaultOAuth2AuthorizationRequestResolver 객체의 resolve 메서드가 호출됩니다.
  • resolve 메서드에서는 OAuth2AuthorizationRequest 객체를 생성하여 리턴합니다. 이 객체에는 리다이렉트 할 로그인 페이지 URL과 관련된 정보 들이 담겨있습니다.
  • OAuth2AuthorizationRequest 객체를 생성하기 위해 먼저 두 가지를 처리합니다.
    • 첫 번째는 요청 URL이 /oauth2/authorization/{registrationId} 패턴과 일치하는지 확인하고 일치하면 registrationId 을 추출합니다. resolveRegistrationId 메서드에서 이 부분을 처리합니다.
    • 두 번째는 요청 파라미터에 action 파라미터가 있는지 확인하고 있다면 추출합니다. 없다면 login 을 리턴합니다.
    • 이후 application.properties 에 정의한 spring.security.oauth2.client.registration 값과 registrationId, action 을 사용하여 OAuth2AuthorizationRequest 객체를 생성하여 리턴합니다.

OAuth2LoginAuthenticationFilter

사용자가 로그인 페이지에서 로그인을 완료하였을 때 OAuth2 제공자 인증 서버에서 인가 코드가 사용자 서비스 백엔드로 리다이렉트 됩니다. 이 때 리다이렉트 요청은 OAuth2LoginAuthenticationFilter 에서 처리됩니다.

아래는 스프링 시큐리티 코드 분석과 디버깅을 통해 그려본 시퀀스 다이어그램입니다.

AbstractAuthenticationProcessingFilter

  • 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 를 호출합니다.

OAuth2LoginAuthenticationFilter

  • 인가 코드로 OAuth2 제공자 인증 서버에서 액세스 토큰을 발급 받고 액세스 토큰으로 사용자 정보를 받아 인증 객체를 생성하여 리턴하는 로직을 처리하는 필터입니다.
  • 토큰을 발급 받고 사용자 정보를 가져오는 부분은 Provider 라는 객체에게 위임합니다. this.getAuthenticationManager().authenticate(authenticationRequest) 코드에서 getAuthenticationManager()ProviderManager 라는 객체를 리턴합니다. ProviderManager 객체는 AuthenticationProvider 인터페이스 구현체를 리스트로 가지고 있고 이 리스트를 순회하면서 인증을 시도하는 객체입니다.

ProviderManager

  • ProviderManager 객체는 AuthenticationProvider 인터페이스의 구현체들을 리스트로 가지고 인증 요청이 왔을 때 Provider 리스트를 순회하여 인증을 지원 하는 Provider 에게 인증을 위임합니다.
  • OAuth2LoginAuthenticationFilter 에서 ProviderManager 호출시 OAuth2LoginAuthenticationToken 을 파라미터로 전달합니다. 따라서 OAuth2LoginAuthenticationProvidersupports 메서드가 조건을 만족하므로 OAuth2LoginAuthenticationProviderauthenticate 메서드가 호출됩니다.
  • 디버거를 통해 확인하면 아래와 같이 3개의 Provider 구현체가 리스트에 포함되어 있습니다.

OAuth2LoginAuthenticationProvider

  • OAuth2LoginAuthenticationProvider 객체는 인가 코드로 OAuth2 제공자 인증 서버에서 액세스 토큰을 발급 받고, 액세스 토큰으로 사용자 정보를 받아 인증 객체를 생성하고 리턴하는 로직을 처리합니다. 이 때 액세스 토큰 발급과 사용자 정보를 받아오는 부분은 각각 OAuth2AuthorizationCodeAuthenticationProvider 객체와 UserService 객체에게 위임합니다.
  • 생성자를 보면 authorizationCodeAuthenticationProvider 참조 변수에 OAuth2AuthorizationCodeAuthenticationProvider 객체를 생성하여 저장합니다. 이 객체에서 인가 코드로 인증 서버에서 액세스 토큰을 받아오는 로직을 수행합니다.
  • 또한 생성자에서 userService 참조 변수에 UserService 객체를 저장합니다. userService 참조 변수는 직접 작성한 코드인CustomOAuth2UserService 클래스의 객체가 저장됩니다. 이 객체에서 액세스 토큰으로 사용자 정보를 받아옵니다.
  • 추가적으로 scope에 "openid" 가 포함되어 있으면 null을 리턴하는 코드가 있습니다. 이 코드 때문에 이전 포스트에서 application.yml 파일에 구글 설정 부분에서 openid,profile,email => profile,email으로 변경 한 것입니다.

OAuth2AuthorizationCodeAuthenticationProvider

  • 이름에서 알 수 있듯이 인가 코드로 OAuth2 제공자 인증 서버에서 액세스 토큰을 발급 받는 로직을 처리합니다.
  • 액세스 토큰 발급은 DefaultAuthorizationCodeTokenResponseClient 객체에게 위임합니다.
  • 발급 받은 토큰을 OAuth2AuthorizationCodeAuthenticationToken 객체로 감싸서 리턴합니다.

DefaultAuthorizationCodeTokenResponseClient

  • 인가 코드로 액세스 토큰을 발급 받기 위한 요청을 생성하고 인증 서버와 통신하여 토큰 응답을 받아오는 로직을 처리합니다.
  • HTTP 통신을 하기 위해 RestTemplate 객체를 생성하고 exchange 메서드를 통해 HTTP 요청을 보냅니다.
  • 토큰을 발급 받기 위한 HTTP 요청 객체 생성은 ClientAuthenticationMethodValidatingRequestEntityConverter 객체에게 위임하고 이 객체는 내부에서 OAuth2AuthorizationCodeGrantRequestEntityConverter 객체에게 위임합니다.

AbstractOAuth2AuthorizationGrantRequestEntityConverter

  • OAuth2AuthorizationCodeGrantRequestEntityConverter 클래스의 상위 클래스입니다. convert 메서드에서 토큰 발급 요청의 헤더와 파라미터를 생성하고 요청 객체를 리턴합니다.
  • 헤더는 OAuth2AuthorizationGrantRequestEntityUtils 객체에서 생성되고, 파라미터는 OAuth2AuthorizationCodeGrantRequestEntityConverter 객체에서 생성됩니다.

OAuth2AuthorizationGrantRequestEntityUtils

  • 토큰 발급 요청의 헤더를 생성하는 객체입니다.
  • application.yml 의 client-authentication-method 필드의 값에 따라서 생성되는 헤더의 값이 달라집니다.

  • client-authentication-method 필드의 값이 client_secret_basic 일 경우 헤더에 clientIdclientSecret 이 추가됩니다. 구글의 경우 client_secret_basic 을 사용합니다. 반면 client_secret_post 일 경우 파라미터에 clientIdclientSecret 이 추가됩니다. 이는 밑에서 알아보겠습니다.

OAuth2AuthorizationCodeGrantRequestEntityConverter

  • 토큰 발급 요청의 파라미터를 생성하는 객체입니다.
  • 아래 코드를 보면 client-authentication-method 필드 값이 client_secret_basic 이 아니면 파라미터에 clientId 를 추가합니다.
  • 또한 client-authentication-method 필드 값이 client_secret_post 이면 파라미터에 clientSecret 을 추가합니다.
  • 따라서 client-authentication-method 필드 값이 client_secret_post 이면 파라미터에 clientIdclientSecret 이 추가됩니다.

CustomOAuth2UserService

  • 이 클래스는 직접 작성한 사용자 클래스입니다. OAuth2LoginAuthenticationProvider 에서 발급 받은 액세스 토큰을 이 객체에게 전달합니다. 따라서 이 객체에서 액세스 토큰으로 OAuth2 제공자 리소스 서버에서 사용자 정보를 얻어오고 정보를 가지고 OAuth2User 인터페이스 구현체인 인증 객체를 생성하여 리턴해야합니다.
  • 이 때 사용자 정보를 리소스 서버로부터 가져오는 과정은 이미 구현되어 있습니다. 상위 클래스인 DefaultOAuth2UserService 객체에게 위임합니다.
  • 이 객체에서는 얻어온 사용자 정보가 각 OAuth2 제공자 별로 형태가 다르기 때문에 서비스에 맞게 정보를 가공하여 동일한 형태인 OAuth2User 구현체로 리턴하는 역할을 수행합니다.

DefaultOAuth2UserService

  • 액세스 토큰으로 리소스 서버에서 사용자 정보를 받아오는 로직을 처리합니다.
  • 리소스 서버에 보낼 HTTP 요청은 OAuth2UserRequestEntityConverter 객체에게 위임합니다.

OAuth2UserRequestEntityConverter

  • 리소스 서버에 보낼 HTTP 요청을 생성하는 로직을 처리합니다.
  • application.properties의 spring.security.oauth2.client.registration.provider.{registrationId}.user-info-authentication-method 값에 따라 액세스 토큰이 포함되는 위치가 달라집니다. header, form, query 값이 있으며 기본 값은 header 입니다.

  • user-info-authentication-method 값이 form 일 경우 POST로 요청하고, 그 외의 값일 경우 GET으로 요청합니다.
  • POST로 요청 할 경우 바디에 엑세스 토큰을 담아서 보내고, GET으로 요청 할 경우 헤더에 액세스 토큰을 담아서 요청합니다.

AbstractAuthenticationProcessingFilter

  • 액세스 토큰으로 사용자 정보를 얻고, CustomOAuth2UserService 에서 OAuth2User 구현체를 리턴하면, OAuth2LoginAuthenticationProvider 에서 OAUth2LoginAuthenticationToken 객체로 감싸서 리턴합니다. 이를 OAuth2LoginAuthenticationFilter 에서 받아서 OAuth2AuthenticationToken 객체로 변환하여 리턴합니다. 상위 클래스인 AbstractAuthenticationProcessingFilter에 정의된 doFilter 메서드에서 받아서 successfulAuthentication 메서드를 호출하게 됩니다.
  • 아래의 successfulAuthentication 메서드에서는 시큐리티 컨텍스트에 인증 객체를 저장합니다. 따라서 다음 필터 부터는 인증 정보를 시큐리티 컨텍스트를 통해 전역적으로 사용할 수 있습니다.
  • 그리고 successHandleronAuthenticationSuccess 메서드를 호출합니다.

OAuth2AuthenticationSuccessHandler

  • 이 클래스는 직접 작성한 사용자 클래스입니다. OAUth2 인증이 성공적으로 끝나면 호출됩니다.
  • 이 객체에서는 인증된 사용자 정보를 기반으로 DB에 사용자 정보를 저장하고, 자체 서비스 토큰을 발급하여 프론트엔드로 리다이렉트 하는 로직을 처리합니다.

4개의 댓글

comment-user-thumbnail
2023년 12월 19일

감사합니다. 혹시 이 게시글에 대한 코드는 없을까요 ?

1개의 답글
comment-user-thumbnail
2023년 12월 22일

안녕하세요.. 제가 처음 소셜로그인과 시큐리티를 oauth2를 사용해서
프로젝트를 구현하면서 참고하면서 보고하는데 혹시 로그인페이지 맨처음부분을
리액트를 안쓰고 jsp로 사용하고싶어서 컨트롤러에 작성하고 loginPage를 추가해줬는데
무한 리다이렉션이 일어납니다.. 권한도 줬는데 혹시 이 부분이 너무 궁금합니다..

1개의 답글