JAVA/Spring Security

(Spring Security) OAuth2 서비스 구현 정리

ri5 2021. 8. 4. 17:18

요구사항

네이버 로그인과 구글 로그인을 사용하기 때문에 이를 구별 해줄 수 있는 서비스를 구현해야한다.

 

서비스 구현

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

OAuth2UserService 타입을 파라미터로 받고 서비스를 설정하기 때문에 반드시 상속 받아야 한다.

 

메서드 오버라이딩

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

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    }

}

유저를 불러오면 판단해야하기 때문에 OAuth2UserService의 메서드인 loadUser를 재정의 해야함. 로그인이 인증이 실패 되어도 동작하는 서비스의 지장을 주면 안되기 때문에 로그인 실패 관련 Exception은 throws함.

 

대리자 생성

@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private final UserRepository userRepository;
    private final HttpSession httpSession;

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

 

DefaultOAuth를 통해 RestOperations으로 UserInfo 엔드포인트에 사용자 속성을 요청해서 사용자 정보를 가져와야하기

때문에 CustomOAuth2UserService.loadUserd의 동작을 대신 해주는 대리자를 만들었다.

 

사용자 정보를 담을 Class 정의

@Getter
public class OAuthAttributes {
    private Map<String,Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;
    private String picture;
}

구글, 네이버 콘솔을 가져오기로 설정한 속성값들을 변수로 선언.

 

@Getter
public class OAuthAttributes {
    private Map<String,Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;
    private String picture;
    
   @Builder
   public OAuthAttributes(Map<String,Object> attributes, String nameAttributeKey, String name, String email, String picture){
     this.attributes = attributes;
     this.nameAttributeKey = nameAttributeKey;
     this.name = name;
     this.email = email;
     this.picture = picture;
   }

   public User toEntity(){
        return User.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.USER)
                .build();
   }
}

빌더를 선언하여 Setter를 굳이 사용하지 않고 빌드할수 있게해줌. 데이터 베이스의 저장할 수 있게 User Entity를 리턴해 주는 toEntity를 구현해준다.

 

@Getter
public class OAuthAttributes {
    private Map<String,Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;
    private String picture;
    
   @Builder
   public OAuthAttributes(Map<String,Object> attributes, String nameAttributeKey, String name, String email, String picture){
     this.attributes = attributes;
     this.nameAttributeKey = nameAttributeKey;
     this.name = name;
     this.email = email;
     this.picture = picture;
   }
   
   //사용자 정보는 Map이기 때문에 변경해야함
   public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String,Object> attributes){
     //네이버 로그인 인지 판단.
     if("naver".equals(registrationId)){
     	return ofNaver("id",attributes);
     }
     return ofGoogle(userNameAttributeName, attributes);
   }
   
   public User toEntity(){
        return User.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.USER)
                .build();
   }
}

네이버 로그인 인지 아닌지 판단해주는 메서드를 생성.

private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
  // 응답 받은 사용자의 정보를 Map형태로 변경.
  Map<String, Object> response = (Map<String, Object>) attributes.get("response");
  // 미리 정의한 속성으로 빌드.
  return OAuthAttributes.builder()
  .name((String) response.get("name"))
  .email((String) response.get("email"))
  .picture((String) response.get("profile_image"))
  .attributes(response)
  .nameAttributeKey(userNameAttributeName)
  .build();
}

public static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String,Object> attributes){
  // 미리 정의한 속성으로 빌드.
  return OAuthAttributes.builder()
  .name((String) attributes.get("name"))
  .email((String) attributes.get("email"))
  .picture((String) attributes.get("picture"))
  .attributes(attributes)
  .nameAttributeKey(userNameAttributeName)
  .build();
}

of 에서 사용되는 메서드를 구현한다. (네이버는 Map<String, Object>형태가 아니기 때문에 따로 타입을 변경해주어야 함)

 

@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private final UserRepository userRepository;
    private final HttpSession httpSession;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        //DefaultOAuth2User 서비스를 통해 User 정보를 가져와야 하기 때문에 대리자 생성
        OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();

        OAuth2User oAuth2User = delegate.loadUser(userRequest);
        //네이버 로그인인지 구글로그인인지 서비스를 구분해주는 코드
        String registrationId = userRequest.getClientRegistration().getRegistrationId();

        //OAuth2 로그인 진행시 키가 되는 필드값 프라이머리키와 같은 값 네이버 카카오 지원 x
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
                                        .getUserInfoEndpoint().getUserNameAttributeName();

        //OAuth2UserService를 통해 가져온 데이터를 담을 클래스
        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        //로그인 한 유저 정보
        User user = saveOrUpdate(attributes);

        //httpSession의 유저 속성을 설정
        httpSession.setAttribute("user", new SessionUser(user));
        // 로그인한 유저를 리턴함
        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
                    attributes.getAttributes(),
                    attributes.getNameAttributeKey());
    }

    //User 저장하고 이미 있는 데이터면 Update
    private User saveOrUpdate(OAuthAttributes attributes) {
        User user = userRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
                .orElse(attributes.toEntity());
        return userRepository.save(user);
    }
}

CustomOAuth2UserService 구현