ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • OAuth2 Client
    카테고리 없음 2024. 3. 7. 19:36

    [ 목차 ]

      설정

      * 환경 spring boot 3.2.1

       

      소셜 로그인을 하려면 먼저 아래와 같은 설정을 해주어야한다.

       

      1. gradle OAuth2 client 의존성 추가

      implementation 'org.springframework.boot:spring-boot-starter-security'
      implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

       

      2. spring application.yaml 에 client 정보 등록

      authorization server(kakao developers) 에 client 로써, client application 을 등록하면 client id, secret 을 받을 수 있다.

      scope 은 고객에게서 수집할 정보로 선택한 항목들이며. 

      redirect uri 는  authorization server 로부터  authorization code 를 전달 받을 uri 로, 애플리케이션을 등록할 때 직접 입력하는 항목이다. 

      /login/oauth2/code 는 OAuth2LoginAuthenticationFilter 의 기본 uri 이다.

      client 로 등록한 정보는 스프링에서 RegisteredClient 로 InMemory 에 저장된다.

      spring:
        application:
          name: oauth2-client
        security:
            oauth2:
              client:
                registration:
                    kakao:
                      client-id: ******
                      client-secret: ******
                      authorization-grant-type: authorization_code
                      redirect-uri: http://localhost:8888/login/oauth2/code/ 
                      scope:
                        - profile_nickname
                        - profile_image
                      client-authentication-method: client_secret_post

       

      ※ 선택할 개인정보의 ID 를 scope 에 명시해줘야하는데 잘못 입력하면 로그인 화면을 띄워줄 때 오류가 난다.

      kakao 에서 고객 정보 수집 동의 사항 선택  scope 에 ID 를 잘못 넣었을 때 로그인 화면 redirect 시 발생하는
      오류
      수집 정보 ID 가 명시되어 있다.






      카카오 개인정보 항목 선택 페이지




      scope 을 잘못 설정했을 때 나는 오류

       


       

      구현

      카카오에 로그인해서 얻는 authorization_code, access_code 등의 정보는 기본적으로 아래와 같은 테이블 구조에 저장된다.

      https://docs.spring.io/spring-security/reference/servlet/appendix/database-schema.html#dbschema-oauth2-client

      CREATE TABLE oauth2_authorized_client (
        client_registration_id varchar(100) NOT NULL, //kakao server
        principal_name varchar(200) NOT NULL,  // user 명
        access_token_type varchar(100) NOT NULL,
        access_token_value blob NOT NULL,
        access_token_issued_at timestamp NOT NULL,
        access_token_expires_at timestamp NOT NULL,
        access_token_scopes varchar(1000) DEFAULT NULL,
        refresh_token_value blob DEFAULT NULL,
        refresh_token_issued_at timestamp DEFAULT NULL,
        created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
        PRIMARY KEY (client_registration_id, principal_name)
      );

       

      spring 에서 default 로는 InMemoryOAuth2AuthorizedClientService 를 사용하고 JdbcOAuth2AuthorizedClientService 로 대체할 수 있는데, 나는 JPA 를 사용하기 위한 구현체를 따로 만들었다.

      import com.sample.oauth.client.repository.JpaAuthorizedClientRepository;
      import lombok.RequiredArgsConstructor;
      import org.springframework.dao.DataRetrievalFailureException;
      import org.springframework.security.core.Authentication;
      import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
      import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
      import org.springframework.security.oauth2.client.registration.ClientRegistration;
      import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
      import org.springframework.security.oauth2.core.OAuth2AccessToken;
      import org.springframework.security.oauth2.core.OAuth2RefreshToken;
      import org.springframework.stereotype.Service;
      import org.springframework.util.StringUtils;
      
      import java.time.Instant;
      import java.util.Collections;
      import java.util.Set;
      
      @Service
      @RequiredArgsConstructor
      public class JpaAuthorizedClientStore implements OAuth2AuthorizedClientService {
          private final JpaAuthorizedClientRepository clientRepository;
          private final ClientRegistrationRepository registrationRepository;
      
      
          @Override
          public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName) {
              AuthorizedClientJpe authorizedClientJpe = clientRepository.findByClientPkClientRegistrationIdAndClientPkPrincipalName(clientRegistrationId, principalName);
              ClientRegistration clientRegistration = this.registrationRepository
                      .findByRegistrationId(clientRegistrationId);
              if (clientRegistration == null) {
                  throw new DataRetrievalFailureException(
                          "The ClientRegistration with id '" + clientRegistrationId + "' exists in the data source, "
                                  + "however, it was not found in the ClientRegistrationRepository.");
              }
              OAuth2AccessToken.TokenType tokenType = null;
              String accessTokenType = authorizedClientJpe.getAccessTokenType();
              if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(accessTokenType)) {
                  tokenType = OAuth2AccessToken.TokenType.BEARER;
              }
              String tokenValue = authorizedClientJpe.getAccessTokenValue();
              Instant issuedAt = authorizedClientJpe.getAccessTokenIssuedAt();
              Instant expiresAt = authorizedClientJpe.getAccessTokenExpiresAt();
              Set<String> scopes = Collections.emptySet();
              String accessTokenScopes = authorizedClientJpe.getAccessTokenScopes();
              if (accessTokenScopes != null) {
                  scopes = StringUtils.commaDelimitedListToSet(accessTokenScopes);
              }
              OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, tokenValue, issuedAt, expiresAt, scopes);
      
              String refreshTokenValue = authorizedClientJpe.getRefreshTokenValue();
              Instant refreshTokenIssuedAt = authorizedClientJpe.getRefreshTokenIssuedAt();
      
              OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(refreshTokenValue, refreshTokenIssuedAt);
      
              return (T) new OAuth2AuthorizedClient(clientRegistration, principalName, accessToken, refreshToken);
          }
      
          @Override
          public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
              ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
              OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
              OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken();
      
              AuthorizedClientJpe authorizedClientJpe = new AuthorizedClientJpe();
              authorizedClientJpe.setClientPk(new ClientPK(clientRegistration.getRegistrationId(), principal.getName()));
              authorizedClientJpe.setAccessTokenType(accessToken.getTokenType().getValue());
              authorizedClientJpe.setAccessTokenValue(accessToken.getTokenValue());
              authorizedClientJpe.setAccessTokenIssuedAt(accessToken.getIssuedAt());
              authorizedClientJpe.setAccessTokenExpiresAt(accessToken.getExpiresAt());
              authorizedClientJpe.setAccessTokenScopes(StringUtils.collectionToDelimitedString(accessToken.getScopes(), ","));
              authorizedClientJpe.setRefreshTokenValue(refreshToken.getTokenValue());
              authorizedClientJpe.setRefreshTokenIssuedAt(refreshToken.getIssuedAt());
              authorizedClientJpe.setCreatedAt(Instant.now());
              clientRepository.save(authorizedClientJpe);
          }
      
          @Override
          public void removeAuthorizedClient(String clientRegistrationId, String principalName) {
              clientRepository.deleteByClientPkClientRegistrationIdAndClientPkPrincipalName(clientRegistrationId, principalName);
          }
      }

       


       

      진행과정

      1. authorization server 에 request 를 보낸다. 

      Get
      {{client-server-uri}}/oauth2/authorization/{{registrationId}}

      registrationId 는 yml 에 등록한 authorization server 의 이름이다.

      http://localhost:8888/oauth2/authorization/kakao

       

      oauth2/authorization 이라는 기본 uri 는 resolver 설정을 통해 원하는 uri 로 변경할 수 있다. 

       private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
                  ClientRegistrationRepository clientRegistrationRepository) {
              DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
                      new DefaultOAuth2AuthorizationRequestResolver(
                              clientRegistrationRepository, "/oauth2/authorization");
              return  authorizationRequestResolver;
          }

       

      아래와 같이 호출하면 authorization server 에서 인증되지 않은 유저에 대해 로그인 페이지를 반환한다. 

      로그인을 하면 client에서 OAuth2AuthorizationRequestRedirectFilter 에서 DefaultOAuth2AuthorizationRequestResolver 를 사용해 authorization_code 발행 요청을 하는 request 객체를 만들어 호출한다.

      request 객체에 들어가는 정보는 yaml 에 설정했던 정보이다.

       

      http://localhost:85/api/v1/oauth2/authorize?response_type=code
      &client_id={{registrationid_clientid}}
      &scope=name
      &state=aoknqLflw9irBhXTkowkBKCUZBGBjmPILj8AhGeNlvs%3D
      &redirect_uri=http://localhost:8888/login/oauth2/code&prompt=consent

       

      해당 request 를 받고 authorization server 에서 redirect_uri 로 설정된 주소로 authorization_code 를 반환해주면, 

      OAuth2LoginAuthenticationFilter 가 authorization_code 정보와 RegisteredClient 정보로

      access_token 을 요청하는 request 를 생성하고 response 를 받는다.

      OAuth2LoginAuthenticationFilter - attemptAuthentication

      박스에 있는 getAuthenticationManager 에서 호출된 ProviderManager 는 OAuth2LoginAuthenticationProvider 를 호출하고

      OAuth2LoginAuthenticationProviderOAuth2AuthorizationCodeAuthenticationProvider를,

      OAuth2AuthorizationCodeAuthenticationProvider는 DefaultAuthorizationCodeTokenResponseClient를 호출해

      access token 을 요청하는 request 를 보낸다. 

      받아온 access token 으로 DefaultOAuth2UserService 에서는 리소스 서버에 userinfo 를 요청해 OAuth2User 를 받는다.

      OAuth2LoginAuthenticationProvider - authenticate

      만약 scopes에 openid 가 포함되어있다면, 위 로직을 타지 않고, OidcAuthorizationCodeAuthenticationProviderOidcUserService 를 통해 OAuth2User 를 로드한다. 

       

       

       

       

       

       

      반응형
    Designed by Tistory.