Programming/Spring

[스프링 부트] Spring Data JPA로 Oauth2 카카오 로그인 구현하기

hyunipad 2022. 9. 14. 23:40
반응형
본 포스팅은 Spring Boot에서 Spring Data JPA를 사용하며 Oauth2 로그인 구현하는 방법을 설명합니다.

이번 포스팅에서는 카카오에서 제공하는 소셜 로그인 구현을 해보도록 하겠습니다.

Oauth2 로그인은 이미 많은 블로거들이 포스팅을 해서 검색을 통해 쉽게 찾을 수 있었지만, 저는 application.yml, Spring Data JPA, gradle를 사용하는 점, Spring Security에서 deprecated 된 메서드들 때문에 새롭게 정리하고자 합니다.

 

개발환경 

  • Spring boot 2.7.0
  • Spring Data JPA
  • Spring Security
  • Oauth2
  • application.yml
  • gradle

build.gradle

Oauth2 로그인을 위한 dependencies 부분은 oauth2-client와 security입니다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

카카오 로그인 API 등록

1) 카카오 디벨로퍼로 이동

https://developers.kakao.com/

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

 

2) 애플리케이션 추가하기

3) REST API 키 확인

4) 좌측메뉴에서 앱 설정 > 플랫폼 > Web 플랫폼 등록

대부분이 8080포트 이겠지만, 설정해준 포트를 입력해주시면 됩니다. ex) localhost:8080, localhost:8060

5) 좌측메뉴에서 제품 설정 > 카카오 로그인

활성화 설정 ON 및 Redirect URI 등록

Redirect URI는 Spring Security를 사용하지 않는 경우에는 컨트롤러에서 URI를 매핑하여 로직을 구현해주면 되지만

Spring Security를 사용하면 Spring에서 자동으로 매핑을 하여 Service 쪽으로 연결해줍니다.

6) 제품 설정 > 카카오 로그인 > 동의항목(닉네임, 카카오 계정)

카카오계정을 필수 동의 하기 위해서는 비즈 앱으로 전환해야 하는데 앱 설정 > 일반에서 앱 아이콘 등록 후에

앱 설정 > 비즈니스에서 개인 개발자 비즈 앱으로 전환할 수 있습니다.

 

Spring Security와 카카오 로그인 API 연동

1) application.yml

구글과 달리 네이버와 카카오는 스프링에서 provider를 직접 입력해줘야 합니다.

provider는 인가 코드, 토큰, 사용자 정보 등을 가져오기 위한 Request URI를 입력해주는 부분입니다.

각각의 URI를 통해 아래와 같은 과정을 거칩니다.

  1. authorization-uri를 통해 인가 코드 발급
  2. token-uri를 호출하여 인가 토큰 발급
  3. user-info-uri를 호출하여 유저 정보 획득

자세한 내용은 아래의 링크를 참고 부탁드리겠습니다.

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: REST API 키
            redirect-uri: "http://localhost:8080/login/oauth2/code/{registrationId}"
            client-authentication-method: POST
            authorization-grant-type: authorization_code
            scope: profile_nickname, account_email #동의 항목
            client-name: Kakao
        provider:
          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

 

 

2) SecurityConfig.java

스프링 시큐리티와 로그인 API를 연동하기 위한 설정 시큐리티 설정입니다.

package com.hyuni.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

import com.hyuni.demo.service.CustomOAuth2UserService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig{
	
	private final CustomOAuth2UserService customOAuth2UserService;
	
	@Bean
	public SecurityFilterChain  filterChain(HttpSecurity http) throws Exception {
		  http.csrf().disable();
		  http.authorizeRequests()
		  	  .anyRequest().permitAll()
//			  .antMatchers("/**").authenticated() // 인가된 사용자만 접근 가능하도록 설정
//			  .antMatchers("게시물등").hasRole(Role.USER.name()) // 특정 ROLE을 가진 사용자만 접근 가능하도록 설정
			  .and()
			  .logout()
			  .logoutSuccessUrl("/")
			  .and()
			  .oauth2Login()
			  .userInfoEndpoint()
			  .userService(customOAuth2UserService);
		  
		  return http.build();
	}

}

 

3) CustomOAuth2UserService.java

OAuthAttributes에서 유저 정보를 가져온 후에 JPA를 통해 회원가입을 처리합니다.

package com.hyuni.demo.service;

import java.util.Collections;

import javax.servlet.http.HttpSession;
import javax.transaction.Transactional;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 com.hyuni.demo.domain.OAuthAttributes;
import com.hyuni.demo.domain.SessionUser;
import com.hyuni.demo.domain.User;
import com.hyuni.demo.repository.UserRepository;

import lombok.RequiredArgsConstructor;

@Service
@Transactional
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService{

	private final UserRepository userRepository;
	private final HttpSession httpSession;
	
	@Override
	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
		
		OAuth2UserService<OAuth2UserRequest, OAuth2User> service = new DefaultOAuth2UserService();
		OAuth2User oAuth2User = service.loadUser(userRequest); // Oath2 정보를 가져옴
		
		String registrationId = userRequest.getClientRegistration().getRegistrationId(); // 소셜 정보 가져옴
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); 
        
        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        User user = saveOrUpdate(attributes);
        httpSession.setAttribute("user", new SessionUser(user));

        return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(user.getRole().getKey())),
                attributes.getAttributes(),
        		attributes.getNameAttributeKey());
	}
	
    private User saveOrUpdate(OAuthAttributes attributes){
        User user =  userRepository.findOneByEmail(attributes.getEmail())
        		.map(entity -> entity.update(attributes.getName()))
        		.orElse(attributes.toEntity());

        return userRepository.save(user);
    }

}

 

4) OAtuhAttributes.java

소셜에서 가져온 유저 정보를 담을 도메인입니다.

package com.hyuni.demo.domain;

import java.util.Map;

import lombok.Builder;
import lombok.Data;
import lombok.Getter;

@Getter
public class OAuthAttributes {
	private Map<String, Object> attributes;
	private String nameAttributeKey;
    private String name;
    private String email;
    
    @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;
    }
    
    public static OAuthAttributes of(String socialName, String userNameAttributeName, Map<String, Object> attributes){
        // 카카오         
        if("kakao".equals(socialName)){
            return ofKakao("id", attributes);
        }
        
        return null;
    }
    
    private static OAuthAttributes ofKakao(String userNameAttributeName, Map<String, Object> attributes) {
        Map<String, Object> kakaoAccount = (Map<String, Object>)attributes.get("kakao_account");
        Map<String, Object> kakaoProfile = (Map<String, Object>)kakaoAccount.get("profile");

        return OAuthAttributes.builder()
                .name((String) kakaoProfile.get("nickname"))
                .email((String) kakaoAccount.get("email"))
                .nameAttributeKey(userNameAttributeName)
                .attributes(attributes)
                .build();
    }
    
    public User toEntity(){
        return User.builder()
                .name(name)
                .email(email)
                .role(Role.USER)
                .build();
    }
}

 

5) SessionUser.java

스프링 시큐리티에서 사용할 세션 정보를 담을 도메인입니다.

package com.hyuni.demo.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
public class SessionUser {

	private String name;
	private String email;
	private String profile_yn;
	
	public SessionUser(User user){
        this.name = user.getName();
        this.email = user.getEmail();
        this.profile_yn = user.getProfile_yn();
    }
}

 

6) UserRepository.java

회원가입 처리를 위한 레포지토리입니다.

package com.hyuni.demo.repository;

import java.util.Optional;

import javax.persistence.EntityManager;
import org.springframework.stereotype.Repository;
import com.hyuni.demo.domain.User;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class UserRepository {

	private final EntityManager em;
	
	public Optional<User> findOneByEmail(String email){
		return em.createQuery("select u from User u where u.email = :email", User.class)
				.setParameter("email", email)
				.getResultList()
				.stream().findAny();
	}
	
	public User save(User user) {
		if(user.getId() == null) {
			user.setProfile_yn("N");
			em.persist(user);
		} else {
			em.merge(user);
		}
		return user;
	}

}

 

7) Oauth2 로그인 호출을 위한 자바스크립트 부분

제가 개발하고 있는 유저 비즈니스는 소셜을 통해 닉네임과 이메일만 받은 후에 추가적인 정보를 입력하여 회원가입을 하는 방식입니다. 코드가 궁금하다면 댓글 부탁드리겠습니다.

 

이어서 구글과 네이버도 같이 하면서 코드를 완성해 가보도록 하겠습니다.

감사합니다.  

반응형