Spring Boot

[Spring] Spring security OAuth2 로그인 구현하기

HEY__ 2024. 8. 6. 16:12
728x90

이 글은 공부를 하면서 알게 된 내용들을 기록하는 글 입니다. 오류나 고쳐야 할 사항들이 있다면 지적 부탁드립니다!

✅ OAuth2 로그인을 구현하는 두 가지 방법

Spring security과 OAuth2를 통해 소셜 로그인을 구현할 수 있는데, 여기에 두 가지 방법이 있습니다.

첫 번째로는 각 소셜(Github, Kakao, Naver 등)에서 제공하는 API를 활용하여 인가 및 코드를 직접 받는 방법,

두 번째로는 Spring에서 제공하는 기능을 사용하는 방법입니다.

 

1️⃣ 소셜에서 제공하는 API 직접 사용하기

Kakao 소셜로그인 과정

 

소셜로그인을 구현하는 첫번째 방법은 소셜에서 제공하는 API를 통해 구현하는 방법입니다.

각 소셜의 Developers 페이지에 가면 소셜 로그인을 하는데에 필요한 과정을 상세하게 설명하고 있습니다.

해당 페이지에서 인가 코드 받기, 토큰 받기와 같은 API를 제공하고 있는데, 이 API를 직접 호출함으로서 소셜로그인을 구현할 수 있습니다.

 

이 방법의 경우 API를 통해 직접 구현하기 때문에 생략된 부분이 없어 흐름을 모두 파악할 수 있다는 장점이 있지만,

소셜로그인마다 제공하는 API가 다르기 때문에, 소셜로그인을 여러 개 지원할 것이라면 추가적인 처리가 필요하다는 단점이 있습니다.

 


2️⃣ Spring의 OAuth2 Client 라이브러리 활용

소셜로그인을 구현하는 두 번째 방법은 Spring의 `OAuth2 Client 라이브러리`를 활용하는 것입니다.

Spring Boot에는 OAuth2 의존성을 추가하여 이를 활용할 수 있습니다.

 

이 방법을 사용하면 `yml` 파일에 oauth2와 관련된 값들을 설정해주면, Spring에서 많은 부분을 처리해줍니다.

 

이 방법의 경우 1번의 방법과는 다르게 API를 직접 호출하지 않아도 되며, 많은 부분을 Spring에서 처리를 해주기 때문에 구현하기 용이하다는 장점이 있지만,

오히려 Spring이 많은 처리를 대신해주기 때문에 오히려 어떤 과정을 거쳐서 구현되는지 파악하기 힘들다는 단점이 있습니다.

 

 

🔥 선택한 방법

두 가지 방법 중, 2번 `Spring OAuth2 Client 라이브러리`를 활용하는 방법을 선택했습니다.

 

첫 번째로는 소셜로그인을 한 개 이상 도입할 예정인데 각 소셜로그인마다 제공하는 가이드라인을 공통적으로 처리하기 힘들 것 같았고,

두 번째로는 Spring에서 제공해주는 라이브러리가 있다면 이를 적극 활용하는 것이 개발품도 덜 들고, 시행착오를 덜 겪을 것 같았기 때문입니다.

 

그럼 2번의 방법을 사용하여 구현하는 방법을 알아봅시다.


프로젝트 환경

Spring Boot 버전: 3.2.1
Spring security 버전: 6.2.1

 

Spring Boot 버전이 3번대로 올라가면서 Spring security의 버전 또한 6번대로 올라갔습니다. 

Spring security의 버전이 6으로 올라가면서, `SecurityConfig`의 `filterChain`에서 필터에 대해 설정을 하는 부분에서 

체인 메서드에서 람다식을 사용하는 방식으로 변경되었습니다.

 

공식 문서를 보면서 람다식을 적용했으니, 참고하면 좋을 것 같습니다 :)


✅ GitHub 설정

소셜로그인을 진행하기 위해서는 먼저 Github 홈페이지에서 간단한 설정이 필요합니다.

 

먼저 소셜로그인을 받을 어플리케이션을 생성해야 합니다. 어플리케이션을 생성할 계정으로 로그인을 해봅시다.

로그인을 했으면 오른쪽 위에 프로필 사진을 누르면 밑과 같은 사진이 뜨는데, 여기에서 `Settings`를 누릅니다.

 

 

Settings를 누르면 계정에 대해 설정할 수 있는 페이지로 들어오는데, 이 때 스크롤을 내리고 `Developer settings` 선택

 

 

`OAuth Apps`를 누르고, 오른쪽의 `New OAuth App`을 눌러 Application 생성

 

 

밑과 같이 Application을 등록하는 페이지가 나올텐데 밑에 적은 내용을 기입해주시면 됩니다.

`Application name`: 원하는 이름 작성

`Homepage URL`: 프론트의 주소를 적어주면 된다.

`Authorization callback URL`: OAuth2 Client 라이브러리를 사용한다면 `{백엔드 url}/login/oauth2/code/github`로  작성

 

 

이후 Register application 버튼을 누르면 `Client ID`와 `Client secrets`가 나오는데, 이 두 값을 복사해둡시다.

`Client ID`는 app에 들어가면 볼 수 있지만, `Client secrets`는 해당 페이지에서 나가면 다시 볼 수 없습니다. (새로운 값 발급 필요)

 


 코드 작성

🔥OAuth2 Client 라이브러리 의존성 추가

`build.gradle`을 연 후, `dependencies`에 밑의 문장을 작성하여, OAuth2 라이브러리의 의존성을 추가합니다.

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

🔥 application.yml 작성

Github에서 application을 생성하고 받은 `Client ID`와 `Client secrets`를 각각 `client-id`와 `client-secret`에 입력.

 

naver, kakao의 경우에는 client-id, client-secret 외에 다른 필드 값도 넣어주어야 합니다. (구글링을 통해 알아보자!)

Github나 Google의 경우에는 `Spring security` 내부에 필요한 값들이 이미 내장되어 있기 때문에 넣어주지 않아도 됩니다 :)

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: sdkjflsjdlfkjsldf
            client-secret: sdkfjlskdjflksjdflkjsldfsdgadsff

 


🔥 User 엔티티 작성

소셜로그인 이후, 서비스만의 자체 회원가입을 진행할 예정인데요,

이를 위해 소셜로그인은 어떤 것을 사용했는지, 식별자는 무엇인지, 사용자의 권한은 무엇인지 저장할 `User` 엔티티를 작성해봅시다.

 

밑의 User 클래스는 저장해야 할 최소한의 정보만 넣어둔 상태니, 각자의 서비스에 맞춰서 추가적으로 값을 넣어줍시다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;

    @NotNull
    @Enumerated(EnumType.STRING)
    private ProviderInfo providerInfo;

    @NotNull
    private String identifier;

    @NotNull
    @Enumerated(EnumType.STRING)
    private Role role;

    ...
}

 

 

사용자가 어떤 소셜 로그인을 사용했는지, 소셜로그인 자체의 식별 코드에 대한 정보를 담고 있는 Enum class입니다.

`attributeKey`는 소셜로부터 전달받은 데이터를 Parsing하기 위해 필요한 key 값,

`providerCode`는 각 소셜은 판별하는 판별 코드,

`identifier`는 소셜로그인을 한 사용자의 정보를 불러올 때 필요한 Key 값입니다.

 

`ProviderInfo.from()` 메서드의 경우, github, kakao, naver, google 중 어떤 소셜로그인에 해당하는지 찾는 정적 메서드입니다.

소셜로그인의 이름을 전달받아, 이를 대문자로 변환하고, Enum class 내의 값들 중 해당하는 값이 있는지 확인 후 반환합니다.

@RequiredArgsConstructor
@Getter
public enum ProviderInfo {
    GITHUB(null, "id", "login"),
    KAKAO("kakao_account", "id", "email"),
    NAVER("response", "id", "email"),
    GOOGLE(null, "sub", "email");

    private final String attributeKey;
    private final String providerCode;
    private final String identifier;

    public static ProviderInfo from(String provider) {
        String upperCastedProvider = provider.toUpperCase();

        return Arrays.stream(ProviderInfo.values())
                .filter(item -> item.name().equals(upperCastedProvider))
                .findFirst()
                .orElseThrow();
    }
}

 

 

 


 

🔥 SecurityConfig 클래스 수정

SecurityConfig 클래스에 `filterChain` 빈을 등록하여 Spring security filter의 전반적인 설정을 할 수 있습니다.

OAuth2 로그인을 적용할 것이기 때문에 `.formLogin()`을 비활성화합니다.

 

중요한 부분은 `.oauth2Login`인데, 여기에서 OAuth2 동작에 대해 어떤 클래스가 처리해줄 것인지 명시하는 부분입니다.

`.successHandler`는 OAuth2 로그인이 성공했을 때 실행할 클래스,

`.failureHandler`는 반대로 OAuth2 로그인이 실패했을 때 실행할 클래스,

`.userInfoEndPoint()`는 Spring의 OAuth2 라이브러리를 통해 OAuth2 로그인이 완료되어, 소셜로부터 사용자의 정보를 받아왔을 때 이를 누가 처리할 것인지 명시하는 곳입니다.

 

SuccessHandler, FailureHandler, OAuthService를 모두 커스텀하여 적용해봅시다.

@Configuration
@Order(1)
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
    private static final String PERMITTED_ROLES[] = {"USER", "ADMIN"};
    private final CustomCorsConfigurationSource customCorsConfigurationSource;
    private final CustomOAuth2UserService customOAuthService;
    private final UserService userService;
    private final OAuth2SuccessHandler successHandler;
    private final OAuth2FailureHandler failureHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.cors(corsCustomizer -> corsCustomizer
                        .configurationSource(customCorsConfigurationSource)
                )
                .csrf(CsrfConfigurer::disable)
                .httpBasic(HttpBasicConfigurer::disable)
                // OAuth 사용으로 인해 기본 로그인 비활성화
                .formLogin(FormLoginConfigurer::disable)
                .authorizeHttpRequests(request -> request
                        .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                        .anyRequest().hasAnyRole(PERMITTED_ROLES))

                // OAuth 로그인 설정
                .oauth2Login(customConfigurer -> customConfigurer
                        .successHandler(successHandler)
                        .failureHandler(failureHandler)
                        .userInfoEndpoint(endpointConfig -> endpointConfig.userService(customOAuthService))
                );

        return http.build();
    }
}

 

💡이 클래스는 CORS 해결을 위해 작성한 클래스입니다!

@Component
public class CustomCorsConfigurationSource implements CorsConfigurationSource {
    private final String ALLOWED_ORIGIN;
    private final List<String> ALLOWED_METHODS = List.of("POST", "GET", "PATCH", "OPTIONS", "DELETE");

    // yml에 프론트의 주소 작성 후 값 주입
    public CustomCorsConfigurationSource(@Value("${url.base}") String BASE_URL) {
        ALLOWED_ORIGIN = BASE_URL;
    }

    @Override
    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Collections.singletonList(ALLOWED_ORIGIN));
        config.setAllowedMethods(ALLOWED_METHODS);
        config.setAllowCredentials(true);
        config.setAllowedHeaders(Collections.singletonList("*"));      
        config.setMaxAge(3600L);
        return config;
    }
}

 

 

🔥 소셜로그인 확장에 대비한 OAuth2UserInfo와 OAuth2UserInfoFactory

소셜 로그인의 종류는 다양합니다. Github 외에도 Google, Kakao, Naver 와 같이 다른 소셜로그인들이 있습니다.

이 소셜로그인들을 모두 적용하고 싶을 때 어떻게 해야 할까요?

 

우선 소셜로그인을 완료하고 나면, OAuth2UserService의 구현체가 실행되는데요,

이 때 parameter로 받은 userRequest를 통해 소셜측으로부터 다양한 정보들을 확인할 수 있습니다.

소셜로그인 완료 후, OAuth2UserService의 parameter(userRequest)에서 확인할 수 있는 값

 

위의 사진은 OAuth2UserService의 구현체에서 breakpoint를 통해 `OAuth2UserRequest`의 값을 캡쳐한 것입니다.

위 정보를 통해 소셜로그인으로부터 사용자의 정보를 받을 수도 있고, 어떤 소셜로그인인지, redirectUri의 정보도 받을 수 있습니다.

 

 

다시 돌아와서, 우리가 소셜로그인을 해서 얻고 싶은 것은

1. 어떤 소셜로그인을 통해서 로그인을 했는지

2. 소셜로그인에 요청했던 사용자의 정보

위의  두 가지입니다.

 

`OAuth2UserRequest`를 통해 받은 위의 두 가지 값을 소셜 로그인의 정보와 상관없이 저장할 객체가 필요할 것 같습니다.

 

`OAuth2UserInfo` 추상 클래스에는 소셜측으로부터 받은 데이터를 저장할 `Map<String, Object> attributes` 변수와,

소셜로그인의 종류를 판별할 `getOAuth2Id()`와 사용자를  판별할 값을 반환하는`getEmail()` 메서드를 선언합니다.

이후 각 소셜타입이 생길 때마다 `OAuth2UserInfo` 추상 클래스를 상속받고 메서드를 구현합니다.

 

 

 

`OAuth2UserInfoFactory`에서는 `OAuth2UserInfo` 생성에 필요한 값을 전달받아 소셜 타입에 따라 적절한 타입의 객체를 생성하도록 합니다.

public class OAuth2UserInfoFactory {
    public static OAuth2UserInfo getOAuth2UserInfo(ProviderInfo providerInfo, Map<String, Object> attributes) {
        switch (providerInfo) {
            case GITHUB -> {
                return new GithubOAuth2UserInfo(attributes);
            }
            case KAKAO -> {
                return new KakaoOAuth2UserInfo(attributes);
            }
            case NAVER -> {
                return new NaverOAuth2UserInfo(attributes);
            }
            case GOOGLE -> {
                return new GoogleOAuth2UserInfo(attributes);
            }
        }
        throw new OAuth2AuthenticationException("INVALID PROVIDER TYPE");
    }
}

 

 

 


🔥 OAuth2UserService 인터페이스 구현: CustomOAuth2UserService

클라이언트가 소셜로그인 페이지에서 특정 로그인 버튼을 눌렀을 때, OAuth2 Client 라이브러리가 많은 일을 처리해줍니다. 

위의 카카오 소셜로그인 과정 다이어그램을 보면 소셜 측의 `Authoriation server`에 Authorization code, Access token를 전달하여 데이터를 받는 과정을 OAuth2 Client 라이브러리가 도맡아줍니다.

출처: https://hudi.blog/oauth-2.0/

 

`OAuth2UserService` 인터페이스를 구현한 `CustomOAuth2UserService`는 소셜측으로부터 Resource를 전달받았을 때(위의 다이어그램에서 13번 상황) 진입합니다.

덕분에 OAuth2 Client 라이브러리를 사용하는 개발자들은 위의 복잡한 과정을 직접 진행하지 않고도, 소셜로그인을 수행할 수 있는 것이죠.

 

`CustomOAuthUserService`에서는 소셜로그인을 통해 전달받은 데이터를 본인의 서비스 니즈에 맞춰서 로직을 작성해봅시다.

이 로직들을 예외없이 무사히 통과한다면, `SecurityConfig`에서 설정한 `SuccessHandler`로 이동합니다.

 

GitGet 서비스에서는 다음과 같은 로직을 가지고 있습니다. 

 

1. OAuth2 Client 라이브러리를 통해 소셜로그인 진행 및 소셜 측으로부터 사용자에 대한 데이터를 받음

2. 어떤 소셜 서비스를 통해 데이터를 받았는지 판별 후, 필요한 데이터를 `Map` 형태로 받음

3. 소셜로그인을 한 사용자의 상태 확인: 가입 여부에 따라 아직 가입을 하지 않았다면 User 객체를 새로 저장

4. SecurityContext에 저장할 객체 생성 후 반환

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    private final UserRepository userRepository;

    @Override
    @Transactional
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        // OAuth2 로그인 진행 시 키가 되는 필드값. Primary Key와 같은 의미.
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint()
                .getUserNameAttributeName();

        // 서비스를 구분하는 코드 ex) Github, Naver
        String providerCode = userRequest.getClientRegistration().getRegistrationId();

        // 어떤 소셜로그인을 사용했는지 반환받는 정적 메서드
        ProviderInfo providerInfo = ProviderInfo.from(providerCode);
        // 소셜쪽에서 전달받은 값들을 Map 형태로 받음
        Map<String, Object> attributes = oAuth2User.getAttributes();

        // 소셜로그인의 종류에 상관없이 사용자의 식별자를 받아오는 코드
        OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(providerInfo, attributes);
        String userIdentifier = oAuth2UserInfo.getUserIdentifier();

        User user = getUser(userIdentifier, providerInfo);

        // Security context에 저장할 객체 생성
        return new UserPrincipal(user, attributes, userNameAttributeName);
    }

    private User getUser(String userIdentifier, ProviderInfo providerInfo) {
        Optional<User> optionalUser = userRepository.findByOAuthInfo(userIdentifier, providerInfo);

        if (optionalUser.isEmpty()) {
            User unregisteredUser = User.builder()
                    .identifier(userIdentifier)
                    .role(Role.NOT_REGISTERED)
                    .providerInfo(providerInfo)
                    .build();
            return userRepository.save(unregisteredUser);
        }
        return optionalUser.get();
    }
}

 

 


🔥 OAuth2SuccessHandler

`CustomOAuth2UserService`에서 예외 없이 모든 로직을 무사히 통과했다면, `SecurityConfig`에서 지정했던 SuccessHandler로 이동한다.

`SimpleUrlAuthenticationSuccessHandler`를 상속하면 `onAuthenticationSuccess()` 메서드를 재정의 해야하는데, 여기에서 소셜로그인이 성공했을 경우에 어떤 액션을 취할 것인지 선언할 수 있다.

 

`CustomOAuth2UserService`에서 반환된 `OAuth2User` 객체는 Spring Security의 Filter를 거치며 `Authentication`에 담긴다.

우리는 이 객체에 담긴 값을 불러와 사용자의 상태에 따라 다른 처리를 하도록 할 것이다.

전체 코드를 하나씩 살펴보자.

1) OAuth2User에서 사용자의 식별자 값을 가지고 옴

2) 식별자 값을 통해 DB에 저장되어 있던 User 엔티티를 가지고 옴
3-1) User의 Role이 NOT_REGISTERED인 경우, 회원가입 페이지로 redirect

3-2) User의 Role이 NOT_REGISTERED가 아닌 경우, 서비스 페이지로 redirect

@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    private final String SIGNUP_URL;
    private final String AUTH_URL;
    private final UserRepository userRepository;

    public OAuth2SuccessHandler(@Value("${url.base}") String BASE_URL,
                                @Value("${url.path.signup}") String SIGN_UP_PATH,
                                @Value("${url.path.auth}") String AUTH_PATH,
                                UserRepository userRepository) {
        this.userRepository = userRepository;
        this.SIGNUP_URL = BASE_URL + SIGN_UP_PATH;
        this.AUTH_URL = BASE_URL + AUTH_PATH;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
        String identifier = oAuth2User.getName();

        User user = userRepository.findByIdentifier(identifier)
                .orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND));

        String redirectUrl = getRedirectUrlByRole(user.getRole(), identifier);
        getRedirectStrategy().sendRedirect(request, response, redirectUrl);
    }

    private String getRedirectUrlByRole(Role role, String identifier) {
        if (role == Role.NOT_REGISTERED) {
            return UriComponentsBuilder.fromUriString(SIGNUP_URL)
                    .queryParam("identifier", identifier)
                    .build()
                    .toUriString();
        }

        return UriComponentsBuilder.fromHttpUrl(AUTH_URL)
                .queryParam("identifier", identifier)
                .build()
                .toUriString();
    }
}

 

1) OAuth2User에서 사용자의 식별자 값을 가지고 옴

OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
String identifier = oAuth2User.getName();

 

 

2) 식별자 값을 통해 DB에 저장되어 있던 User 엔티티를 가지고 옴

User user = userRepository.findByIdentifier(identifier)
    .orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND));

 

3-1) User의 Role이 NOT_REGISTERED인 경우, 회원가입 페이지로 redirect

3-2) User의 Role이 NOT_REGISTERED가 아닌 경우, 서비스 페이지로 redirect

    private String getRedirectUrlByRole(Role role, String identifier) {
        if (role == Role.NOT_REGISTERED) {
            return UriComponentsBuilder.fromUriString(SIGNUP_URL)
                    .queryParam("identifier", identifier)
                    .build()
                    .toUriString();
        }

        return UriComponentsBuilder.fromHttpUrl(AUTH_URL)
                .queryParam("identifier", identifier)
                .build()
                .toUriString();
    }

 

특정 페이지로 리다이렉트를 하기 위해서는 URI를 생성해야 하는데, `UriComponentsBuilder`를 통해 이를 생성할 수 있다.

`SIGNUP_URL`과 `AUTH_URL`은 yml에 작성한 값으로서, 회원가입 페이지 혹은 서비스 페이지의 URL을 담고 있는 값이다.

 

getRedirectStrategy().sendRedirect(request, response, redirectUrl);

 

`getRedirectUrlByRole()`을 통해 전달받은 `redirectUrl`을 파라미터로 전달하여 해당 URL로 리다이렉트하게 된다.

 


🔥 OAuth2FailureHandler

소셜로그인을 하는 과정에서 문제가 발생하여, 인증을 완료하지 못했따면 SecurityConfig에서 지정한 FailureHandler가 실행된다.

 

소셜로그인 과정은 프론트 팀원분들과 의논할 부분이 많았는데,

우리 팀은 소셜로그인 과정에서 문제가 발생한 경우, 밑과 같은 과정을 거치기로 했다.

1) 로그인 화면으로 돌아가기

2) Query string을 통해 error 메세지 전달

 

@Component
public class OAuth2FailureHandler extends SimpleUrlAuthenticationFailureHandler {
    private final String REDIRECT_URL;
    private final String ERROR_PARAM_PREFIX = "error";

    public OAuth2FailureHandler(@Value("${url.base}") String REDIRECT_URL) {
        this.REDIRECT_URL = REDIRECT_URL;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        // 리다이렉트할 주소 생성, query string에 에러 메세지 추가
        String redirectUrl = UriComponentsBuilder.fromUriString(REDIRECT_URL)
                .queryParam(ERROR_PARAM_PREFIX, exception.getLocalizedMessage())
                .build()
                .toUriString();

        getRedirectStrategy().sendRedirect(request, response, redirectUrl);
    }
}

 

 


🔥 OAuth2User 인터페이스 구현: UserPrincipal 

`CustomOAuth2UserService`에서는 이후의 인증 과정을 위해 `OAuth2User` 객체를 반환해야 한다.

`OAuth2User` 인터페이스를 구현하여 커스텀하고, 이 객체를 활용하자.

 

`OAuth2User`인터페이스를 구현한 `DefaultOAuth2User` 클래스를 사용하지 않고, 굳이 커스텀하는 이유는 다음과 같다.

Spring security filter를 지나면서 Authentication 객체에 인증과 관련된 정보를 담게 되는데, 직접 커스텀을 하여 서비스에서 필요하다고 판단되는 값들을 넣어 이후에 Authentication 객체에서 뽑아쓰기 위함이다.

 

`OAuth2User` 인터페이스를 구현하면, `getName()` 메서드를 오버라이드하게 되는데 나는 이 때 User 엔티티의 식별자 값으로 사용하기로 했던 identifier를 반환하도록 했다.

@Getter
public class UserPrincipal implements OAuth2User {

    private User user;
    private String nameAttributeKey;
    private Map<String, Object> attributes;
    private Collection<? extends GrantedAuthority> authorities;

    public UserPrincipal(User user) {
        this.user = user;
        this.authorities = Collections.singletonList(new SimpleGrantedAuthority(user.getRole().getKey()));
    }

    public UserPrincipal(User user, Map<String, Object> attributes, String nameAttributeKey) {
        this.user = user;
        this.authorities = Collections.singletonList(new SimpleGrantedAuthority(user.getRole().getKey()));
        this.attributes = attributes;
        this.nameAttributeKey = nameAttributeKey;
    }

    /**
     * OAuth2User method implements
     */
    @Override
    public String getName() {
        return user.getIdentifier();
    }
}

 

 

🔥 테스트 해보기

이후 프론트 주소(ex: http://localhost:3000)에 들어가면 GitHub 로그인 페이지가 나오고, 로그인 버튼을 눌러 깃허브 소셜로그인이 가능해진다! :)

 

728x90