[스프링 부트] Spring Data JPA로 Oauth2 카카오 로그인 구현하기
본 포스팅은 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) 카카오 디벨로퍼로 이동
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를 통해 아래와 같은 과정을 거칩니다.
- authorization-uri를 통해 인가 코드 발급
- token-uri를 호출하여 인가 토큰 발급
- user-info-uri를 호출하여 유저 정보 획득
자세한 내용은 아래의 링크를 참고 부탁드리겠습니다.
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
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 로그인 호출을 위한 자바스크립트 부분
제가 개발하고 있는 유저 비즈니스는 소셜을 통해 닉네임과 이메일만 받은 후에 추가적인 정보를 입력하여 회원가입을 하는 방식입니다. 코드가 궁금하다면 댓글 부탁드리겠습니다.
이어서 구글과 네이버도 같이 하면서 코드를 완성해 가보도록 하겠습니다.
감사합니다.