본문 바로가기
ToyProject

스프링 게시판 만들기 - 8 (2) (소셜 로그인 [구글,네이버,카카오,페이스북] )

by 완두완두콩 2022. 8. 2.

스프링 게시판 만들기 - 8 (1) 편과 이어지는 내용입니다.

 

각각의 개발자 센터에 들어가서

클라이언트 Id 와 비밀번호를 받았다는 가정하에

진행합니다!

 

 

구글


CustomOAuth2MemberService.java

package toyproject.board.config.auth;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import toyproject.board.config.auth.provider.FacebookUserInfo;
import toyproject.board.config.auth.provider.GoogleUserInfo;
import toyproject.board.config.auth.provider.OAuth2UserInfo;
import toyproject.board.detail.PrincipalDetails;
import toyproject.board.domain.Member;
//import toyproject.board.dto.OAuthAttributes;
//import toyproject.board.dto.SessionMember;
import toyproject.board.domain.Role;
import toyproject.board.dto.MemberDto;
import toyproject.board.repository.MemberRepository;
import toyproject.board.service.MemberService;

import javax.servlet.http.HttpSession;
import java.util.Collections;
import java.util.Optional;

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

    private final MemberRepository memberRepository;
    private final HttpSession httpSession;
    private final MemberService memberService;



    // 함수 종료시 @AuthenticationPrincipal 어노테이션이 만들어진다.
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        OAuth2UserService delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        System.out.println("userRequest = " + userRequest.getAccessToken().getTokenValue());
        // 구글 로그인버튼 클릭 -> 구글 로그인창 -> 로그인을 완료 -> code 를 리턴(Oauth-client 라이브러리) -> AccessToken 요청
        // userRequest 정보 -> loadUser함수 호출 -> 구글로부터 회원프로필을 받아준다.
        System.out.println("userRequest = " + userRequest.getClientRegistration());
        System.out.println("oAuth2User = " + userRequest.getAdditionalParameters());
        System.out.println("oAuth2User.getAttributes() = " + oAuth2User.getAttributes());
        System.out.println("oAuth2User.getAuthorities() = " + oAuth2User.getAuthorities());
        System.out.println("oAuth2User.getName() = " + oAuth2User.getName());

//        OAuth2UserInfo oAuth2UserInfo = null;
//        if(userRequest.getClientRegistration().getRegistrationId().equals("google")){
//            System.out.println("구글 로그인 요청");
//            oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
//        }else if(userRequest.getClientRegistration().getRegistrationId().equals("facebook")){
//            System.out.println("페이스북 로그인 요청");
//            oAuth2UserInfo = new FacebookUserInfo(oAuth2User.getAttributes());
//        }else{
//            System.out.println("구글과 페이스북만 지원");
//        }

        String provider = userRequest.getClientRegistration().getRegistrationId(); // google
        String providerId = oAuth2User.getAttribute("sub");
        String username = provider + "_" + providerId; // google_1923912312312
        String password = "dodokong";
        String email = oAuth2User.getAttribute("email");
        String role = Role.USER.getValue();
//        String provider = oAuth2UserInfo.getProvider(); // google
//        String providerId = oAuth2UserInfo.getProviderId();
//        String username = provider + "_" + providerId; // google_1923912312312
//        String password = "dodokong";
//        String email = oAuth2UserInfo.getEmail();
//        String role = Role.USER.getValue();



        Member member = memberRepository.findByUsername(username);
        if (member == null) {
            System.out.println("최초 가입");
            member = Member.builder()
                    .username(username)
                    .password(password)
                    .email(email)
                    .role(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();
            memberService.joinUserWithMember(member);
        }else {
            System.out.println("이미 가입한 적이 있습니다.");
        }
        return new PrincipalDetails(member, oAuth2User.getAttributes());
    }
}

주석 처리한 부분은 제외하고 확인하면, 이전에 로그를 찍었을 때 sub 가 primary key 값으로 들어가 있었고,

email 은 email 정보를 가져오고.. 결국 로그의 값에 맞추어서 값을 꺼내오고

해당 값을 builder 를 이용해서 Join 을 했다.

 

로그인 하면 위와 같이 정보가 저장됨을 확인할 수 있다!

페이스북 


application.properties 에 본인의 클라이언트 id 와 패스워드를 등록하고!!

페이스북 로그인 버튼을 새로 만들어준다.

로그인을 테스트 해보면 위와 같은 결과가 나온다.

구글과 차이점은 sub 가 id 로 바뀌었다는 것이다.

 

구글에서 작성한 코드를 복사 붙여넣기로

만들어도 좋지만, 클린코딩을 위해서 다른 방법을 써보고자 합니다.

 

OAuth2UserInfo.interface

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

우리가 회원가입에 필요한 값은 위의 4개이다.

각각의 플랫폼 별로 모두 가져와야 하는 값이기에 인터페이스로 만들고,

 

GoogleUserInfo.java

public class GoogleUserInfo implements OAuth2UserInfo{

    private Map<String, Object> attributes; // getAttributes

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

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

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

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

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

FacebookUserInfo.java

public class FacebookUserInfo implements OAuth2UserInfo{

    private Map<String, Object> attributes; // getAttributes

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

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

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

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

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

CustomOAuth2MemberService 에서 attributes 값을 각각의 Info 클래스에 넘겨받아서 

메소드를 구현한다.

이 때, getProvider() 값을 각각 정의해서 넣어주면 끝!

package toyproject.board.config.auth;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import toyproject.board.config.auth.provider.FacebookUserInfo;
import toyproject.board.config.auth.provider.GoogleUserInfo;
import toyproject.board.config.auth.provider.OAuth2UserInfo;
import toyproject.board.detail.PrincipalDetails;
import toyproject.board.domain.Member;
//import toyproject.board.dto.OAuthAttributes;
//import toyproject.board.dto.SessionMember;
import toyproject.board.domain.Role;
import toyproject.board.dto.MemberDto;
import toyproject.board.repository.MemberRepository;
import toyproject.board.service.MemberService;

import javax.servlet.http.HttpSession;
import java.util.Collections;
import java.util.Optional;

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

    private final MemberRepository memberRepository;
    private final HttpSession httpSession;
    private final MemberService memberService;



    // 함수 종료시 @AuthenticationPrincipal 어노테이션이 만들어진다.
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        OAuth2UserService delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        System.out.println("userRequest = " + userRequest.getAccessToken().getTokenValue());
        // 구글 로그인버튼 클릭 -> 구글 로그인창 -> 로그인을 완료 -> code 를 리턴(Oauth-client 라이브러리) -> AccessToken 요청
        // userRequest 정보 -> loadUser함수 호출 -> 구글로부터 회원프로필을 받아준다.
        System.out.println("userRequest = " + userRequest.getClientRegistration());
        System.out.println("oAuth2User = " + userRequest.getAdditionalParameters());
        System.out.println("oAuth2User.getAttributes() = " + oAuth2User.getAttributes());
        System.out.println("oAuth2User.getAuthorities() = " + oAuth2User.getAuthorities());
        System.out.println("oAuth2User.getName() = " + oAuth2User.getName());

        OAuth2UserInfo oAuth2UserInfo = null;
        if(userRequest.getClientRegistration().getRegistrationId().equals("google")){
            System.out.println("구글 로그인 요청");
            oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
        }else if(userRequest.getClientRegistration().getRegistrationId().equals("facebook")){
            System.out.println("페이스북 로그인 요청");
            oAuth2UserInfo = new FacebookUserInfo(oAuth2User.getAttributes());
        }else{
            System.out.println("구글과 페이스북만 지원");
        }

//        String provider = userRequest.getClientRegistration().getRegistrationId(); // google
//        String providerId = oAuth2User.getAttribute("sub");
//        String username = provider + "_" + providerId; // google_1923912312312
//        String password = bCryptPasswordEncoder.encode("dodokong");
//        String email = oAuth2User.getAttribute("email");
//        String role = Role.USER.getValue();
        String provider = oAuth2UserInfo.getProvider(); // google
        String providerId = oAuth2UserInfo.getProviderId();
        String username = provider + "_" + providerId; // google_1923912312312
        String password = "dodokong";
        String email = oAuth2UserInfo.getEmail();
        String role = Role.USER.getValue();



        Member member = memberRepository.findByUsername(username);
        if (member == null) {
            System.out.println("최초 가입");
            member = Member.builder()
                    .username(username)
                    .password(password)
                    .email(email)
                    .role(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();
            memberService.joinUserWithMember(member);
        }else {
            System.out.println("이미 가입한 적이 있습니다.");
        }
        return new PrincipalDetails(member, oAuth2User.getAttributes());
    }
}

그러면 각각 구글 로그인과 페이스북 로그인이 들어올 때마다

DB 에 저장되는 값을 손쉽게 설정할 수 있다!

 

앞으로도 네이버와 카카오 Info 를 추가해주기만 하면 끝!!

 

네이버


네이버는 설정 값이 조금 다르다.

글로벌한 서비스인 구글, 페이스북 등 과는 달리, 네이버나 카카오의 경우

기본 설정 값이 없기 때문에  다음과 같이 provider 정보까지 모두 적어줘야 한다.

scope 를 제외한 값들은 모두 기본 설정 값이므로 변경 없이 사용하면 됩니다!

여기서 우리가 눈여겨 봐야할 부분은 바로 response 이다.

웬 뜬금없는 response ? 하고 회원 가입 url 을 만든 후 로그인을 진행하면

Missing Attribute Response 라는 오류가 발생하게 된다.

 

이는 네이버가 유저 정보를 json 형태로 response 안에 담아줘서 발생하는 오류다.

 이처럼, 유저 정보를 response 안에 담아줘서 전송하기 때문에, 우리가 꺼낼 때 또한 response 안에서 꺼내줘야 한다.

 

NaverUserInfo.java

package toyproject.board.config.auth.provider;

import java.util.Map;

public class NaverUserInfo implements OAuth2UserInfo{

    private Map<String, Object> attributes; // getAttributes

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

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

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

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

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

 

 

CustomOAuth2MemberService.java

정상적으로 DB 에 저장됨을 알 수 있다.

 

 

카카오


카카오도 앞선 작업들과 동일하게 카카오 개발자 센터에서 앱을 등록하고,

application.properties 에 등록하고

html 파일에 링크를 추가해준 뒤, 어떤 값이 넘어오는지 확인해본다.

로그를 찍어보면 위와 같은 값이 나온다. 카카오의 경우에 값이 좀 많다!..

우리가 필요한 정보는 id, nickname, 그리고 email 이다.

id 는 그냥 attributes.get()으로 꺼내면 될 것 같고, nickname 은 attributes.get(properties)

email 은 attributes.get(kakao_account) 로 꺼내면 될 것 같다.

 

KakaoUserInfo.java

package toyproject.board.config.auth.provider;

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 getProviderId() {
        return attributes.get("id").toString();
    }

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

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

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

 

똑같이 Info 를 작성해주고, 다른 점은 앞서 말했듯 attributes.get(properties) 요렇게

값을 꺼내오는 부분만 차이가 난다.

서비스 부분에 카카오만 추가해주면 완료!!

 

 

 

 

사담


내용이 내가 예상한 것보다 더 방대해서 조금 놀랐다..

처음에 어떻게 해야할지 갈피를 못잡아서 많이 헤맸는데 다행히 구현을 잘 완료해서 다행이다.

앞으로 진행할 토이프로젝트나 기존 프로젝트들에도 잘 적용할 수 있을 것 같은 느낌!..

 

지금 내가 구현한 코드는 여러 Oauth 방식 중에서도 

Authorization Code Grant(권한 부여 승인 코드 방식) 이다.

다른 방법에 대해서도 좀 더 알아보고, JWT 를 이용한

액세스 토큰과 리프레시 토큰도 적용해보면 좋겠다는 생각중이다!..

 

 

(게시판 구현이라고 해놓고, 로그인만 주구장창 구현하는 것 같아서 문제..)

 

댓글