회원가입은 입력 하라는 게 너무 많아 (2)

이지훈·2022년 11월 2일
0

Spring Security

목록 보기
2/5

저번에 이어서 customOAuth2UserService 를 살펴보겠습니다.

📂 customOAuth2UserService

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

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;


    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {

        OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService();

        OAuth2User oAuth2User = oAuth2UserService.loadUser(oAuth2UserRequest);

        // 어떤 소셜로그인을 이용하는지 구분 하기위해 쓰임.
        // ex) registrationId = "naver", registrationId = "google" 등등
        String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();


        // OAuth2 로그인 시 키 값이 된다.
        // 구글은 키 값이 "sub"이고, 네이버는 "response"이고, 카카오는 "id"이다.
        // 각각 다르므로 이렇게 따로 변수로 받아서 넣어줘야함.
        String userNameAttributeName = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();

        // OAuth2 로그인을 통해 가져온 OAuth2User의 attribute를 담아주는 of 메소드.
        // oAuth2User.getAttributes() 에는 다음 값이 담긴다.
        // 구글 예시) { sub=1231344534523565757, name=홍길동, given_name=길동, family_name=홍, picture=https://xxx, email=xxx@gmail.com, email_verified=true, locale=ko}
        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        String encode = passwordEncoder.encode(UUID.randomUUID().toString());

        // user 예시) username=google_1231344534523565757 , password = $2a$10$nlkgA6oUE.7KpHSO6tDDpOBth4PICf1DeQiHQ2qbbaA8o3s1osGvG
        User user = new User(registrationId+"_"+attributes.getUsername(), encode ,registrationId);


        User loadOrSaveUser = loadOrSave(user);

        return new CustomIntegratedLogin(loadOrSaveUser,attributes);

    }

    // 이미 저장된 유저라면 load, 아니면 save
    private User loadOrSave(User user) {
        Optional<User> loadUser = userRepository.findByUsername(user.getUsername());

        if (loadUser.isEmpty()){ // 소셜로그인을 시도한 아이디가 없다면 저장하고
            return userRepository.save(user);
        } else { // 소셜로그인을 시도한 아이디가 있다면 가져온다
            return loadUser.get();
        }
    }
}

우선 customOAuth2UserService 는 OAuth2UserService<OAuth2UserRequest, OAuth2User> 을 구현하고 있습니다 이것을 왜 구현하고 있냐??

시큐리티 기본흐름에서 5,6번에 해당하는 내용입니다. 기본 로그인은 UserDetailsService 를 통해 UserDetails를 가져오고 있습니다.

그러나 소셜로그인은 OAuth2UserService 를 이용하여 OAuth2User 를 가져오기때문에 OAuth2UserService 를 구현해야 합니다.

OAuth2UserService 는 인터페이스로 loadUser 라는 메서드를 가지고 있고 이를 오버라이딩해서 구현 해주어야 합니다.

DefaultOAuth2UserService 를 생성한 이유는 그 안에 있는 loadUser 를 통해
OAuth2User 를 가져올수 있기때문에 생성하였습니다.

그 다음 주석을 읽다가 보면 OAuthAttributes.of(~); 가 있는것을 볼 수 있습니다.
그럼 OAuthAttributes 를 살펴보겠습니다.

📂 OAuthAttributes

@Getter
public class OAuthAttributes {
    private Map<String, Object> attributes; // ex) { sub=1231344534523565757, name=홍길동, given_name=길동, family_name=홍, picture=https://xxx, email=xxx@gmail.com, email_verified=true, locale=ko}
    private String nameAttributeKey; // ex) { google="sub", kakao="id", naver="response" }
    private String name;  // ex) 홍길동
    private String email;  // ex) test@gmail.com
    private String username; // ex) 1231344534523565757


    public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email,String username) {
        this.attributes = attributes;
        this.nameAttributeKey = nameAttributeKey;
        this.name = name;
        this.email = email;
        this.username=username;

    }


    // 해당 로그인인 서비스가 kakao 인지 google 인지 구분하여, 알맞게 매핑을 해주도록 합니다.
    // registrationId는 소셜로그인을 시도한 서비스 명("google","kakao","naver"..)이 되고,
    // userNameAttributeName은 해당 서비스의 map의 키 값이 되는 값이 됩니다. { google="sub", kakao="id", naver="response" }
    public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
        if (registrationId.equals("kakao")) {
            return ofKakao(userNameAttributeName, attributes);
        } else if (registrationId.equals("naver")) {
            return ofNaver(userNameAttributeName,attributes);
        }
        return ofGoogle(userNameAttributeName, attributes);
    }

    // 카카오 예시
    // attributes = {
    //          id=3498023493,     이 부분을 가져온다(username)
    //          connected_at=2022-07-17T12:40:28Z,
    //          properties={nickname=xxx},
    //          kakao_account={
    //                   profile_nickname_needs_agreement=false,
    //                   profile={nickname=xxx},    이 부분을 가져온다
    //                   has_email=true,
    //                   email_needs_agreement=false,
    //                   is_email_valid=true,
    //                   is_email_verified=true,
    //                   email=xxx@nate.com   이 부분을 가져온다
    //                }
    //          }
    private static OAuthAttributes ofKakao(String userNameAttributeName, Map<String, Object> attributes) {
        Map<String, Object> kakao_account = (Map<String, Object>) attributes.get("kakao_account");  // 카카오로 받은 데이터에서 계정 정보가 담긴 kakao_account 값을 꺼낸다.
        Map<String, Object> profile = (Map<String, Object>) kakao_account.get("profile");   // 마찬가지로 profile(nickname 등) 정보가 담긴 값을 꺼낸다.
        String username = attributes.get(userNameAttributeName).toString();
        return new OAuthAttributes(attributes,
                userNameAttributeName,
                (String) profile.get("nickname"),
                (String) kakao_account.get("email"),
                username
               );
    }

    // 네이버 예시
    // attributes = {
    //          resultcode=00,
    //          message=success,
    //          response= {
    //               id=lkdslsdkfjsdf-sdf98334twfdgdfv,  이 부분을 가져온다
    //               nickname=xx,
    //               email=xx@naver.com,    이 부분을 가져온다
    //               name=xxx     이 부분을 가져온다
    //               }
    //         }
    private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
        Map<String, Object> response = (Map<String, Object>) attributes.get("response");    // 네이버에서 받은 데이터에서 프로필 정보다 담긴 response 값을 꺼낸다.
        return new OAuthAttributes(attributes,
                userNameAttributeName,
                (String) response.get("name"),
                (String) response.get("email"),
                (String)response.get("id")
               );
    }

    private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {

        String username = attributes.get(userNameAttributeName).toString();
        return new OAuthAttributes(attributes,
                userNameAttributeName,
                (String) attributes.get("name"),
                (String) attributes.get("email"),username
               );
    }

}

OAuthAttributes 는 여러 필드를 가지고 있는데 주석에 예시를 적어놓았으니 쓱 보고 넘어 가시면 됩니다.

앞서 호출한 of 메서드를 살펴보면, 우선 어떤서비스로 왔는지 구분하여 처리하고 있습니다 뭔가 복잡해보이는 카카오를 살펴보겠습니다.

ofKakao() 에서 이를 처리하고 있습니다.

attributes 가 넘어오는데 주석문과 같이 값이 넘어오게 됩니다.
id, connected_at, properties, kakao_account 등이 key 값이 되며 = 옆에 있는게 value 값이 됩니다. 그런데 kakao_account 는 값이 여러개 있는것을 볼 수 있는데 이건 뭘까요??

단순히 생각해서 value값이 또 key-value 형태의 쌍으로 넘어온 것 입니다 그럼 profile, has_email, email 등이 다시 키 값이 되는거겠죠?? 간단하죠??

다시 ofKakao() 의 첫줄을 보면 kakao_account 를 Map<String, Object> 의 형태로 받고있습니다 당연하겠죠? value값이 key-value 쌍으로 넘어왔기 때문이죠.

같은 원리로 profile 또한 동일하게 넘어오기 때문에 Map으로 받습니다.

attributes.get(userNameAttributeName) 은 attributes.get("id") 랑 같은 의미입니다.

이해가 되셨다면 리턴값의 구성을 전부 아시게 된겁니다. 다시 CustomOAuth2UserService 로 넘어가겠습니다.

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

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;


    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {

        OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService();

        OAuth2User oAuth2User = oAuth2UserService.loadUser(oAuth2UserRequest);

        // 어떤 소셜로그인을 이용하는지 구분 하기위해 쓰임.
        // ex) registrationId = "naver", registrationId = "google" 등등
        String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();


        // OAuth2 로그인 시 키 값이 된다.
        // 구글은 키 값이 "sub"이고, 네이버는 "response"이고, 카카오는 "id"이다.
        // 각각 다르므로 이렇게 따로 변수로 받아서 넣어줘야함.
        String userNameAttributeName = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();

        // OAuth2 로그인을 통해 가져온 OAuth2User의 attribute를 담아주는 of 메소드.
        // oAuth2User.getAttributes() 에는 다음 값이 담긴다.
        // 구글 예시) { sub=1231344534523565757, name=홍길동, given_name=길동, family_name=홍, picture=https://xxx, email=xxx@gmail.com, email_verified=true, locale=ko}
        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        String encode = passwordEncoder.encode(UUID.randomUUID().toString());

        // user 예시) username=google_1231344534523565757 , password = $2a$10$nlkgA6oUE.7KpHSO6tDDpOBth4PICf1DeQiHQ2qbbaA8o3s1osGvG
        User user = new User(registrationId+"_"+attributes.getUsername(), encode ,registrationId);


        User loadOrSaveUser = loadOrSave(user);

        return new CustomIntegratedLogin(loadOrSaveUser,attributes);

    }

    // 이미 저장된 유저라면 load, 아니면 save
    private User loadOrSave(User user) {
        Optional<User> loadUser = userRepository.findByUsername(user.getUsername());

        if (loadUser.isEmpty()){ // 소셜로그인을 시도한 아이디가 없다면 저장하고
            return userRepository.save(user);
        } else { // 소셜로그인을 시도한 아이디가 있다면 가져온다
            return loadUser.get();
        }
    }
}

또 User 라는게 등장하는데 별거 없습니다.

주석문 예시 형태로 넘어가게 됩니다.
그 다음, 이전에 동일한 카카오계정으로 로그인 한 적이 있다면 불러오고 없다면 저장해서 가져오는 간단한 메소드를 거치고 난 후

new CustomIntegratedLogin(loadOrSaveUser,attributes); 라는 것을 거쳐 시큐리티가 알아서 저장하는데 CustomIntegratedLogin 을 확인해 봐야겠죠??

📂 CustomIntegratedLogin

public class CustomIntegratedLogin implements OAuth2User, UserDetails {

    private User user;
    private OAuthAttributes oAuthAttributes;


    public CustomIntegratedLogin(User user) {
        this.user=user;
    }

    public CustomIntegratedLogin(User user, OAuthAttributes oAuthAttributes) {
        this.user=user;
        this.oAuthAttributes=oAuthAttributes;

    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }

    @Override
    // ex) {sub=1231344534523565757, name=홍길동, given_name=길동, family_name=홍, picture=https://xxx, email=xxx@gmail.com, email_verified=true, locale=ko}
    public Map<String, Object> getAttributes() {
        return oAuthAttributes.getAttributes();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });
        return collection;
    }

    @Override
    // ex) 1231344534523565757
    public String getName() {
        return oAuthAttributes.getUsername();
    }
}

시작부터 OAuth2User, UserDetails 를 구현한다고 적혀 있습니다.
이 둘을 하나로 묶은 이유는 일반 로그인이랑 소셜로그인 둘 다 이용하는 상황에서
따로 나누면 코드를 더 작성해야 해서 하나로 묶었습니다.

그리고 하나로 묶으면 SuccessHandler 를 같이 쓸 수있는 장점이 있습니다.

다시 본론으로 돌아와서

   public CustomIntegratedLogin(User user, OAuthAttributes oAuthAttributes) {
       this.user=user;
       this.oAuthAttributes=oAuthAttributes;

   }

이 생성자를 이용해서 객체를 만들어 주면 시큐리티에서 저장하고 SuccessHandler 에서 객체를 받아서 후 처리를 진행해 주게 됩니다.

다음 글 에서 SuccessHandler 부분을 살펴보겠습니다

0개의 댓글