본문 바로가기
Projects/SNS프로젝트

[소셜로그인] 페이스북, 구글, 네이버, 카카오 구현

by 젊은오리 2022. 12. 20.
728x90

 

프로젝트에 open auth를 지원하는 4사(페이스북, 구글, 네이버, 카카오)에 대해 소셜로그인 기능을 추가해보았다.

각각의 개발자 센터에 들어가서 application을 만들고, client-id와 client-secret id를 받았다는 전제하에 진행할 것이다.

 

1. pom.xml에 oath 라이브러리 추가

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

 

 

2. application.yml설정

  • scope에 적힌 public_profile, email와 같은 명칭은 정해진 형식이니 개발자센터에서 확인해야 한다.
  • scope에는 각 리소스 서버(구글, 카카오 등)의 user DB에서 가져오고 싶은 정보를 명시한다.
  • 네이버, 카카오와 같은 경우는 provider를 따로 적어줘야 spring이 지원하는 oath2 login을 활용할 수 있다.
spring:
  security:
    oauth2:
      client:
        provider: #네이버, 카카오는 따로 provider를 작성해야한다.
          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize
            token-uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user-name-attribute: response
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id

        registration:
          facebook:
            client-id: [Facebook 클라이언트 ID]
            client-secret: [Facebook 클라이언트 시크릿 ID]
            scope: 
            - public_profile
            - email

          google:
            clientId: [Google 클라이언트 ID]
            clientSecret: [Google 클라이언트 시크릿 ID]
            redirect-uri: http://localhost:8080/login/oauth2/code/google
            scope:
              - email
              - profile

          naver:
            clientId: [Naver 클라이언트 ID]
            client-secret: [Naver 클라이언트 시크릿 ID]
            redirect-uri: http://localhost:8080/login/oauth2/code/naver
            authorization-grant-type: authorization_code
            scope:
              - name
              - email
              - profile_image
            client-name: Naver

          kakao:
            clientId: [Kakao 클라이언트 ID]
            client-secret: [Kakao 클라이언트 시크릿 ID]
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            authorization-grant-type: authorization_code
            scope:
              - profile_nickname
              - account_email
              - profile_image
            client-name: Kakao
            client-authentication-method: POST

 

 

3. Security Class - configure수정

userService에는소셜 로그인 성공 시 진행할 OAuth2UserService 인터페이스의 구현체를 등록한다. 리소스 서버(네이버, 카카오, 페이스북 등) 에서 사용자 정보를 가져온 상태에서 추가 진행하고자 하는 기능을 구현하면 된다.

@Override
protected void configure(HttpSecurity http) throws Exception {
   
   //super삭제 - 기존 시큐리티가 가지고 있는 기능이 다 비활성화됨.
   http.csrf().disable();
   http.authorizeRequests()
      .antMatchers("/user/**","/chatroom/**","/image/**","/subscribe/**","/comment/**","/api/**").authenticated()
      .anyRequest().permitAll()
      .and()
   .formLogin()
      .loginPage("/auth/signin")
      .loginProcessingUrl("/auth/signin")
      .defaultSuccessUrl("/")
      .and()
      .oauth2Login() //oauth2로그인도 추가로 진행
      .userInfoEndpoint() //oauth2로그인 성공 후에 사용자 정보를 바로 가져온다.
      .userService(oAuth2DetailsService);
}

 

 

 

위의 과정을 모두 마쳤다면, 이제 OAuth2UserService 인터페이스의 구현체(나의 경우, oAuth2DetailsService)를 직접 커스텀 하면된다.

바로 이 구현체에서 모든 소셜로그인 로직이 실행되게 되는데, 가령 사용자가 구글로그인을 할 경우에는 구글로그인이 되도록 해야 할 것이고, 카카오로그인을 할 경우에는 카카오 로그인이 진행될 수 있도록 구현해야 한다.

아래는 이미 완성된 코드인데, 전체적으로 어떻게 흘러가는지 파악하고 내려가자.

  • 사용자를 저장할 userRepository를 불러온다.
  • super.loadUser(userRequest)를 통해서 OAuth2User타입의 사용자 정보를 불러온다. (OAuth2User.getAttributes()를 사용하면 Map<String,Object>의 형태로 사용자 정보가 담겨진 것을 확인할 수 있다.)
  • userRequest.getClientRegistration().getRegistrationId()로 리소스서버를 구분할 수 있다.
  • OAuth2UserInfo(나중에 만들어야 하는 인터페이스)형의 참조변수를 선언해서 GoogleUserInfo, FacebookUserInfo, NaverUserInfo, KakaoUserInfo(나중에 만들어야 하는 oAuth2UserInfo의 구현체)의 인스턴스를 참조하게 한다.
  • 최종적으로 oAuth2UserInfo에는 리소스 서버 중 하나의 사용자 정보가 들어가 있을것이다.
  • oAuth2UserInfo에 담긴 정보를 이용해서 각자의 회원가입에 필요한 변수들을 setting한 다음, OAuth2User형태로 리턴해주면 된다. (나의 경우, 사전에 OAuth2User의 인스턴스로 PrincipalDetail이라는 클래스를 미리 커스텀했다.) 
package com.cos.photogramstart.config.oauth;

import java.util.Map;
import java.util.UUID;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import com.cos.photogramstart.config.auth.PrincipalDetail;
import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.domain.user.UserRepository;

import lombok.RequiredArgsConstructor;

@Slf4j
@RequiredArgsConstructor
@Service
public class OAuth2DetailsService extends DefaultOAuth2UserService{
   
   private final UserRepository userRepository;
   
   @Override
   public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

      OAuth2User oauth2User = super.loadUser(userRequest);

      OAuth2UserInfo oAuth2UserInfo = null;
      if(userRequest.getClientRegistration().getRegistrationId().equals("google")){
         log.info("구글 로그인 요청");
         oAuth2UserInfo = new GoogleUserInfo(oauth2User.getAttributes());

      }else if(userRequest.getClientRegistration().getRegistrationId().equals("facebook")){
         log.info("페이스북 로그인 요청");
         oAuth2UserInfo = new FacebookUserInfo(oauth2User.getAttributes());
      }else if(userRequest.getClientRegistration().getRegistrationId().equals("naver")){
         log.info("네이버 로그인 요청");
         oAuth2UserInfo = new NaverUserInfo((Map)oauth2User.getAttributes().get("response"));
      }else if(userRequest.getClientRegistration().getRegistrationId().equals("kakao")){
         log.info("카카오 로그인 요청");
         oAuth2UserInfo = new KakaoUserInfo(oauth2User.getAttributes());
      }

      String username = oAuth2UserInfo.getProvider() + oAuth2UserInfo.getUsername();
      String password = new BCryptPasswordEncoder().encode(UUID.randomUUID().toString());
      String email = oAuth2UserInfo.getEmail();
      String name = oAuth2UserInfo.getName();


      User userEntity = userRepository.findByUsername(username);

      if(userEntity==null) { //최초 로그인 시
         User user = User.builder()
               .username(username)
               .password(password)
               .email(email)
               .name(name)
               .role("ROLE_USER")
               .build();
         return new PrincipalDetail(userRepository.save(user));
      }else {
         return new PrincipalDetail(userEntity);
      }
   }
}

 

 

회원가입을 하기 위해 필요로 하는 사항을 체크한다. 우선 나의 프로젝트에서 회원가입에 필요한 컬럼을 살펴보니, username, password, email, name 4가지이다. 따라서 어떤 리소스 서버인지를 나타내는 provider를 포함한 5가지에 대해서 정의할 수 있는 인터페이스를 생성한다.

public interface OAuth2UserInfo {
    String getProvider();
    String getUsername();
    String getPassword();
    String getEmail();
    String getName();
}

 

 

이후, 이를 구현한 구현체 4가지를 만들어준다.(Google, KaKao, Naver, Facebook)

<GoogleUserInfo>

package com.cos.photogramstart.config.oauth;

import java.util.Map;

public class GoogleUserInfo implements OAuth2UserInfo{
    private Map<String, Object> attributes;
    public GoogleUserInfo(Map<String, Object> attributes){
        this.attributes = attributes;
    }

    @Override
    public String getProvider() {
        return "google_";
    }

    @Override
    public String getUsername() {
        return (String) attributes.get("sub");
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }
}

 

<KakaoUserInfo>

package com.cos.photogramstart.config.oauth;

import java.util.Map;

public class KakaoUserInfo implements OAuth2UserInfo{

    private Map<String, Object> attributes; // getAttributes
    private Map<String, Object> attributesProperties; // getAttributes
    private Map<String, Object> attributesAccount; // getAttributes
    private Map<String, Object> attributesProfile; // getAttributes

    public KakaoUserInfo(Map<String,Object> attributes){
        this.attributes = attributes;
        this.attributesProperties = (Map<String, Object>) attributes.get("properties");
        this.attributesAccount = (Map<String, Object>) attributes.get("kakao_account");
        this.attributesProfile = (Map<String, Object>) attributesAccount.get("profile");
    }

    @Override
    public String getProvider() {
        return "kakao_";
    }

    @Override
    public String getUsername() {
        return attributes.get("id").toString();
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getEmail() {
        return (String) attributesAccount.get("email");
    }

    @Override
    public String getName() {
        return (String) attributesProperties.get("nickname");
    }
}

 

<NaverUserInfo>

package com.cos.photogramstart.config.oauth;

import java.util.Map;

public class NaverUserInfo implements OAuth2UserInfo{

    private Map<String, Object> attributes;
    public NaverUserInfo(Map<String,Object> attributes){
        this.attributes = attributes;
    }

    @Override
    public String getProvider() {
        return "naver_";
    }

    @Override
    public String getUsername() {
        return (String) attributes.get("id");
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }
}

 

<FacebookUserInfo>

package com.cos.photogramstart.config.oauth;

import java.util.Map;

public class FacebookUserInfo implements OAuth2UserInfo{

    private Map<String, Object> attributes;
    public FacebookUserInfo(Map<String,Object> attributes){
        this.attributes = attributes;
    }

    @Override
    public String getProvider() {
        return "facebook_";
    }

    @Override
    public String getUsername() {
        return (String) attributes.get("id");
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }
}

 

 

 

모든게 완료되었고, 각각의 소셜 로그인이 제대로 동작되는 지 프로젝트에서 확인해보자.

 

728x90

댓글