Java/Spring

(24.12.29) 카카오 로그인 구현(Rest API, Spring Boot) : 밑준비

Job_E 2024. 12. 29. 15:50

서술을 시작하기 앞서, 해당 내용은 KaKao Developers의 카카오 로그인 서비스를 기반으로 서술됨을 알린다.

(※ url : https://developers.kakao.com/docs/latest/ko/index)

 

 

요즘 지인과 프로젝트로 앱을 만들고 있으며, 나는 현재 백엔드 포지션을 잡고 개발을 맡고 있다.

그 과정에서 필요한 기능 중 하나가 SNS 로그인인데, 크게 카카오, 네이버, 구글 애플로 잡았다.

 

이 중 애플을 제외한 카카오, 네이버, 구글은 다 똑같이 OAuth 2.0 기반의 로그인 방식을 사용하고 세세한 부분에서만 다르기 때문에 카카오 로그인 기능 구현 하나만 제대로 알고 간다면 다른 두 포털의 로그인 API도 무난하게 구현할 수 있을 거라고 생각한다.(실제로 본인도 카카오 로그인 소스코드를 조금만 바꿔가면서 다른 두 포탈의 로그인 API를 구현했다.)

 

우선 카카오 API 로그인의 로직부터 알아보자.

출저 : 카카오 디벨로퍼스 > 카카오 로그인 > 서비스 로그인 과정

 

간단히 요약하면, 

- (클라이언트)카카오 로그인 요청

- (서버)사전 발급해준 인가 코드 확인 및 인가 토큰(Access Token) 발급

- (플랫폼) 인가 토큰 확인 및 사용자 정보 서버로 발급

- (서버 > 클라이언트) 사용자 정보 제공 및 로그인 완료.

 

크고 단순하게만 보자면 다음과 같고, 이젠 조금 디테일하게 살펴보자.

 

0. 로그인 구현 시작 전 밑준비

카카오나 네이버나 구글에서 제공하는 로그인 서비스의 공통점을 2가지 정도만 언급해보자면,

- OAuth 2.0 기반의 서비스라는 것

- 다음과 같은 요소가 사전에 준비되어 있어야 한다는 것

>> Client Id, Client Secret, Redirect URI

(※ 이외에도 받아야 할 파라미터가 더 있지만... 중요하지 않으니 넘어가자.)

 

위 세 요소에 대해 간단히 설명을 하자면, 

- Client ID : 앱의 Rest API 키

- Client Secret : 토큰 발급과 갱신을 위해 사용되는 코드

- Redirect URI : 서비스에서 요청한 인가 코드와 토큰을 전달하기 위한 URI

 

OAuth 2.0은 Spring Security를 활용하여 별도 구현하면 되고, 그 아래의 3가지 정보는 각 서비스의 개발 지원 포탈에서 일련의 과정을 거쳐 발급받을 수 있다.

(※ 네이버 : https://developers.naver.com/main/)

(※ 구글 : https://cloud.google.com/?hl=ko > 콘솔)

(※ 카카오 : https://developers.kakao.com/)

 

구글과 네이버에 대한 설명은 생략하고, 여기엔 카카오 로그인에 대한 과정만 알아보자.

 

1) 카카오 디벨로퍼스에서 내 애플리케이션으로 이동

 

2) 애플리케이션 추가 진행

필요한대로 적어넣자..

 

애플리케이션 등록이 끝났다면, Client Id와 Client Secret를 확인할 수 있다.

Client ID : Rest API 구현 기준, 여기서 REST API 키가 우리가 찾는 Client ID다.
Client Secret : 내 애플리케이션 > 비즈니스 인증 > 보안 에서 발급 및 확인 가능

Client Secret의 정보 조회 및 변경은 해당 애플리케이션을 만든 유저 본인만 가능하며, 아래의 '활성화 상태' 옵션을 '사용함'으로 바꿔야 Client Secret이 사용 가능해진다.

 

Redirect URI : 내 애플리케이션 > 앱 설정 > 플랫폼 측 Web 도메인 등록 후 발급 가능(이하 이미지 생략)

사이트 도메인엔 현재 Rest API를 구현하고 있는 백엔드 도메인을 등록한 뒤 등록 가능하다.

 

위의 과정을 통해 Client ID, Client Secret, Redirect URI를 얻을 수 있었다.

이제 이 데이터를 이용해서 Spring Boot에서의 밑준비를 진행해보자.

 

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // (2024.11.17) OAuth 2.0
    implementation 'org.springframework.boot:spring-boot-starter-security'  // (2024.11.17)
    //thymeleaf
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

    // json 파싱 목적으로 jackson 선언
    implementation 'com.fasterxml.jackson.core:jackson-databind'

    //webClient 사용을 위한 Webflux 추가
    implementation 'org.springframework.boot:spring-boot-starter-webflux'

    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
}

다음과 같이 선언해주자.

 

Application.properties

spring.security.oauth2.client.registration.kakao.client-id=
spring.security.oauth2.client.registration.kakao.client-secret=
spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post
spring.security.oauth2.client.registration.kakao.redirect-uri=
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.client-name=kakao
spring.security.oauth2.client.registration.kakao.scope=

spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute=id

 

위에서 공백인 client-id와 client-secret은 이전 단계에서 본인이 설정한 데이터를 집어넣으면 되고,

Scope의 경우 https://developers.kakao.com/console/app/1088566/product/login/scope 을 참조하여 입력하자.

(ex. spring.security.oauth2.client.registration.kakao.scope=profile_nickname, account_email)

 

Spring Security

카카오 로그인은 OAuth2.0 기반의 서비스이므로 SecurityConfig를 선언해주자.

import kr.trablock.backend.api.auth.service.OAuth2UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    private final OAuth2UserService oAuth2UserService;

    public SecurityConfig(OAuth2UserService oAuth2UserService) {
        this.oAuth2UserService = oAuth2UserService;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf((csrf) ->
                csrf.disable());

        http.authorizeHttpRequests((config) -> config.anyRequest().permitAll());
        http.oauth2Login((oauth2Configurer) -> oauth2Configurer
                .loginPage("/login")
                .successHandler(successHandler())
                .userInfoEndpoint(userInfoEndpointConfig ->
                        userInfoEndpointConfig.userService(oAuth2UserService)));


        return http.build();
    }

    @Bean
    AuthenticationSuccessHandler successHandler() {
        return((request, response, authentication) -> {
            DefaultOAuth2User defaultOAuth2User = (DefaultOAuth2User) authentication.getPrincipal();

            String id = defaultOAuth2User.getAttributes().get("id").toString();
            String body  = """
                    {"id":"%s"}
                    """.formatted(id);

            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());


            PrintWriter writer = response.getWriter();
            writer.println(body);
            writer.flush();
        });
    }
}

 

OAuth2UserService

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class OAuth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

        // Role Generate
        List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_ADMIN");

        // nameAttributeKey
        String userNameAttributeName = userRequest.getClientRegistration()
                .getProviderDetails()
                .getUserInfoEndpoint()
                .getUserNameAttributeName();


        return new DefaultOAuth2User(authorities, oAuth2User.getAttributes(), userNameAttributeName);
    }

}

 

이것으로 밑준비는 다 끝났고, 이후부터 본격적으로 카카오 로그인 서비스를 구현해보자.