OAuth2UserService 의
OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) 메서드는
서드파티에 사용자 정보를 요청할 수 있는 access token 을 얻고나서 실행됩니다.

이 때 access token과 같은 정보들이 oAuth2UserRequest 파라미터에 들어있습니다.

이 메서드가 할 일은 다음과 같습니다.

  1. access token을 이용해 서드파티 서버로부터 사용자 정보를 받아온다.
  2. 해당 사용자가 이미 회원가입 되어있는 사용자인지 확인한다.
    만약 회원가입이 되어있지 않다면, 회원가입 처리한다.
    만약 회원가입이 되어있다면, 프로필사진URL 등의 정보를 업데이트한다.
  3. UserPrincipal 을 return 한다.
    세션 방식에서는 여기서 return한 객체가 시큐리티 세션에 저장된다.
    하지만 JWT 방식에서는 저장하지 않는다.
    (JWT 방식에서는 인증&인가 수행시 HttpSession을 사용하지 않을 것이다.)


1. 커스텀 OAuth2UserService 클래스 생성

DefaultOAuth2UserService를 상속해서 만들 것입니다.

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    /**
     * 서드파티 접근을 위한 accessToken까지 얻은다음 실행된다.
     */
    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) {
        return null;
    }
}

 

2. super.loadUser(oAuth2UserRequest)

처음에 loadUser 메서드의 역할을 1 ~ 3번으로 정의했었는데요.
그 중 1번이었던 "access token을 이용해 서드파티 서버로부터 사용자 정보를 받아온다" 기능을 넣을 것입니다.

아주 쉽습니다. super.loadUser() 를 호출하기만하면 됩니다.
DefaultOAuth2UserService 클래스의 loadUser() 메서드에 이 기능이 구현되어있기 때문입니다.

@Service  
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) {
        // accessToken으로 서드파티에 요청해서 사용자 정보를 얻어옴
        OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);

        return null;
    }
}

 

3. 사용자 정보 validation

앞서 OAuth2User 타입 객체로 전달받은 사용자 정보를
간단하게 validation 해주도록 하겠습니다.

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) {
        OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);

        validateAttributes(oAuth2User.getAttributes());

        return null;
    }

    private void validateAttributes(Map<String, Object> attributes) {
        if (!attributes.containsKey("email")) {
            throw new IllegalArgumentException("서드파티의 응답에 email이 존재하지 않습니다!!!");
        }
    }
}

 

4. 자동 회원가입 / 회원정보 갱신

처음에 정의했던 loadUser 메서드의 역할 2번을 구현해볼 것입니다.

회원가입 한 적이 있는지 확인해서
회원가입한 적 없으면 자동으로 회원가입을 시키고,
회원가입한 적 있으면 정보갱신을 하도록 구현....하려고 했으나 제 테스트 프로젝트에서는 갱신할 정보가 없네요.
프로필사진 url 같은걸 쓴다면 이런걸 갱신해주면 됩니다.

package com.yelim.security.service;

import com.yelim.security.domain.Provider;
import com.yelim.security.domain.User;
import com.yelim.security.domain.UserPrincipal;
import com.yelim.security.repository.UserRepository;
import java.util.Map;
import java.util.Optional;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;

    public CustomOAuth2UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) {
        Map<String, Object> attributes = super.loadUser(oAuth2UserRequest).getAttributes();
        Provider provider = Provider.from(oAuth2UserRequest.getClientRegistration().getRegistrationId());

        validateAttributes(attributes);

        registerIfNewUser(attributes, provider);
        
        return null;
    }

    private void validateAttributes(Map<String, Object> userInfoAttributes) {
        if (!userInfoAttributes.containsKey("email")) {
            throw new IllegalArgumentException("서드파티의 응답에 email이 존재하지 않습니다!!!");
        }
    }

    private void registerIfNewUser(Map<String, Object> userInfoAttributes, Provider provider) {
        String email = (String) userInfoAttributes.get("email");

        Optional<User> optionalUser = userRepository.findByEmailAndProvider(email, provider);

        if (optionalUser.isPresent()) {
            return;
        }
        User user = new User(null, email, "대충 아무 텍스트", provider);
        userRepository.save(user);
    }
}

.

5. UserPrincipal 리턴

loadUser 메서드는 OAuth2User 타입을 return 합니다.

 

만약 인증/인가를 세션 방식으로 구현하면,

return 한 OAuth2User 객체가 시큐리티 세션에 저장됩니다.


JWT 방식으로 구현할 경우 세션을 사용하지 않으므로 세션에 저장하지는 않습니다.

 

하지만 그렇다고해서 지금처럼 null 을 리턴하면 안됩니다.

스프링 시큐리티 코드 어딘가에서 이 객체에 접근하기 때문에

그 부분이 실행될 때 Null Pointer Exception이 발생합니다.

 

package com.yelim.security.service;

import com.yelim.security.domain.Provider;
import com.yelim.security.domain.User;
import com.yelim.security.domain.UserPrincipal;
import com.yelim.security.repository.UserRepository;
import java.util.Map;
import java.util.Optional;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;

    public CustomOAuth2UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) {
        Map<String, Object> attributes = super.loadUser(oAuth2UserRequest).getAttributes();
        Provider provider = Provider.from(oAuth2UserRequest.getClientRegistration().getRegistrationId());

        validateAttributes(attributes);

        User user = registerIfNewUser(attributes, provider);

        return UserPrincipal.create(user, attributes);
    }

    private void validateAttributes(Map<String, Object> userInfoAttributes) {
        if (!userInfoAttributes.containsKey("email")) {
            throw new IllegalArgumentException("서드파티의 응답에 email이 존재하지 않습니다!!!");
        }
    }

    private User registerIfNewUser(Map<String, Object> userInfoAttributes, Provider provider) {
        String email = (String) userInfoAttributes.get("email");

        Optional<User> optionalUser = userRepository.findByEmailAndProvider(email, provider);

        if (optionalUser.isPresent()) {
            return optionalUser.get();
        }
        User user = new User(null, email, "대충 아무 텍스트", provider);
        return userRepository.save(user);
    }
}

 

이제 OAuth2UserService 구현이 끝났습니다!

 

5. 테스트코드

도대체 OAuth2UserService 는 테스트코드를 어떻게 짜야할까요?...

언젠가 알게되면 꼭 정리하겠습니다....

+ Recent posts