프로젝트/Profee

[SpringBoot] Kakao 소셜 로그인 구현: OAuth2.0 & Spring Security 활용

승요나라 2024. 9. 11. 16:01

Google 소셜 로그인에 이어 Kakao 소셜 로그인을 구현하려고 한다.

Kakao 소셜 로그인은 Google 때 구현해놓았던 틀을 기반으로 아주 간단하게 추가하므로 기반 코드가 없다면 아래 포스팅을 참고하면 좋겠다.

 

[SpringBoot] Google 소셜 로그인 구현: OAuth2.0 & Spring Security 활용

코드 구현에 앞서, 소셜 로그인 개념은 아래 포스팅을 참고하면 좋겠다. Spring Security & OAuth2.0 & JWT Token 소셜로그인 개념 정리Spring Security 란?Spring Security는 애플리케이션의 보안을 담당하는 프레

seung-yo.tistory.com

 

 

 

폴더 구조

Kakao 소셜 로그인은 이전 포스팅을 따라 틀을 갖춰 놓았다면 따로 추가할 필요가 없다. 이번 시간에는 KakaoOauth.java, application.properties 파일만 수정할 예정이다.

 

 

 

 

 

소셜 로그인 요청 Redirect 처리 (Kakao)

- KakaoOauth 클래스 수정

Google 로그인 때와 비슷하게 사용자가 카카오 인증을 요청할 수 있는 URL을 생성하고, 받은 인증 코드를 이용해 액세스 토큰을 요청하는 기능을 제공하는 코드이다. RestTemplate과 HttpURLConnection 두 가지 방법으로 액세스 토큰을 요청할 수 있으며, 성공적으로 토큰을 받으면 이를 콘솔에 출력하는 코드를 추가했다.

 

 

KakaoOauth.java

package com.example.Profee.service.social;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

// 공통 interface를 구현할 소셜 로그인 각 타입별 Class 생성 (Kakao)
@Component
@RequiredArgsConstructor
public class KakaoOauth implements SocialOauth {
    @Value("${sns.kakao.url}")
    private String KAKAO_SNS_BASE_URL;
    @Value("${sns.kakao.client.id}")
    private String KAKAO_SNS_CLIENT_ID;
    @Value("${sns.kakao.callback.url}")
    private String KAKAO_SNS_CALLBACK_URL;
    @Value("${sns.kakao.token.url}")
    private String KAKAO_SNS_TOKEN_BASE_URL;

    @Override
    public String getOauthRedirectURL() {
        Map<String, Object> params = new HashMap<>();
        params.put("response_type", "code");
        params.put("client_id", KAKAO_SNS_CLIENT_ID);
        params.put("redirect_uri", KAKAO_SNS_CALLBACK_URL);

        String parameterString = params.entrySet().stream()
                .map(x -> x.getKey() + "=" + x.getValue())
                .collect(Collectors.joining("&"));

        return KAKAO_SNS_BASE_URL + "?" + parameterString;
    }

    @Override
    public String requestAccessToken(String code) {
        RestTemplate restTemplate = new RestTemplate();

        Map<String, Object> params = new HashMap<>();
        params.put("grant_type", "authorization_code");
        params.put("client_id", KAKAO_SNS_CLIENT_ID);
        params.put("redirect_uri", KAKAO_SNS_CALLBACK_URL);
        params.put("code", code);

        ResponseEntity<String> responseEntity =
                restTemplate.postForEntity(KAKAO_SNS_TOKEN_BASE_URL, params, String.class);

        if (responseEntity.getStatusCode() == HttpStatus.OK) {
            // 콘솔에 받은 토큰 출력
            String tokenResponse = responseEntity.getBody();
            System.out.println("Received Access Token: " + tokenResponse);

            return responseEntity.getBody();
        }
        return "카카오 로그인 요청 처리 실패";
    }

    public String requestAccessTokenUsingURL(String code) {
        try {
            URL url = new URL(KAKAO_SNS_TOKEN_BASE_URL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            conn.setDoOutput(true);

            Map<String, Object> params = new HashMap<>();
            params.put("grant_type", "authorization_code");
            params.put("client_id", KAKAO_SNS_CLIENT_ID);
            params.put("redirect_uri", KAKAO_SNS_CALLBACK_URL);
            params.put("code", code);

            String parameterString = params.entrySet().stream()
                    .map(x -> x.getKey() + "=" + x.getValue())
                    .collect(Collectors.joining("&"));

            BufferedOutputStream bous = new BufferedOutputStream(conn.getOutputStream());
            bous.write(parameterString.getBytes());
            bous.flush();
            bous.close();

            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));

            StringBuilder sb = new StringBuilder();
            String line;

            while ((line = br.readLine()) != null) {
                sb.append(line);
            }

            if (conn.getResponseCode() == 200) {
                // 콘솔에 받은 토큰 출력
                String tokenResponse = sb.toString();
                System.out.println("Received Access Token: " + tokenResponse);

                return tokenResponse;
            }
            return "카카오 로그인 요청 처리 실패";
        } catch (IOException e) {
            throw new IllegalArgumentException("알 수 없는 카카오 로그인 Access Token 요청 URL 입니다 :: " + KAKAO_SNS_TOKEN_BASE_URL);
        }
    }
}

 

 

카카오 소셜 로그인에서 액세스 토큰을 요청하는 두 가지 방법이 있는데, 바로 Spring의 RestTemplate자바 기본 API인 HttpURLConnection을 사용하는 방법이다. 이 두 방법의 주요 차이점은 사용의 편의성과 코드 복잡성에 있다. 물론 두 방법을 모두 구현할 필요는 없지만 공부 차원에서 메서드를 남겨두었다. (Google 소셜 로그인도 동일하게 남겨두었다.)

 

1. RestTemplate 방식 (requestAccessToken)

  • Spring 프레임워크의 고수준 HTTP 클라이언트: RestTemplate은 Spring에서 제공하는 HTTP 통신을 위한 유틸리티 클래스로, REST API 호출을 간단하게 처리할 수 있도록 설계되었다.
  • 장점:
    • 코드가 간결하고 읽기 쉬움.
    • 다양한 HTTP 메서드(GET, POST, PUT, DELETE 등)를 간편하게 사용할 수 있음.
    • 예외 처리 및 응답 매핑이 내장되어 있어 개발자가 수동으로 처리할 필요가 없음.
    • Spring 프로젝트에서는 RestTemplate을 사용하면 일관된 코드 스타일을 유지할 수 있음.
  • 단점:
    • Spring에 의존적이기 때문에, Spring 프레임워크가 필요 없는 상황에서는 사용이 부적절할 수 있음.

 

2. HttpURLConnection 방식 (requestAccessTokenUsingURL)

  • 자바 기본 API: HttpURLConnection은 자바 표준 API로, 네트워크 통신을 저수준에서 직접 제어할 수 있는 클래스를 제공한다.
  • 장점:
    • 자바 표준 API만으로 동작하므로, 외부 라이브러리에 의존하지 않음.
    • 네트워크 요청에 대한 세부 제어가 가능하므로, 매우 세밀한 설정이나 통신 요구가 있을 때 적합함.
  • 단점:
    • 코드가 복잡하고 장황함.
    • 예외 처리, 응답 매핑 등을 수동으로 해야 하므로 실수할 가능성이 있음.
    • 유지보수가 어렵고, 코드 가독성이 떨어짐.
 

RestTemplate VS. HttpURLConnection

  • RestTemplate: 코드가 간결하고, Spring 애플리케이션에서 쉽게 통합할 수 있는 방식으로, 유지보수 및 가독성 면에서 유리하다. Spring 기반 프로젝트에서는 주로 이 방식을 권장한다.
  • HttpURLConnection: 더 저수준에서 네트워크 통신을 제어할 수 있지만, 코드가 복잡해지고 실수할 가능성이 크며 유지보수가 어렵다. 특별한 이유가 없다면 이 방식은 피하는 것이 좋다.

따라서,  두 가지 방법을 모두 사용할 필요는 없으며 RestTemplate만 사용하는 것이 더 효율적이다.

 

 

 

 

 

application.properties 파일 설정

Google 때와 동일하게 Kakao를 통해 발급받은 클라이언트 아이디(= Rest API 키)와 시크릿을 입력해주면 된다. 편의상 Google과 Kakao를 구분하는 주석을 추가했다.

 

 

application.properties

# Google
sns.google.url=https://accounts.google.com/o/oauth2/v2/auth
sns.google.client.id=구글 클라이언트 아이디
sns.google.client.secret=구글 클라이언트 시크릿
sns.google.callback.url=http://localhost:8080/auth/google/callback
sns.google.token.url=https://oauth2.googleapis.com/token

# Kakao
sns.kakao.url=https://kauth.kakao.com/oauth/authorize
sns.kakao.client.id=카카오 클라이언트 아이디
sns.kakao.client.secret=카카오 클라이언트 시크릿
sns.kakao.callback.url=http://localhost:8080/auth/kakao/callback
sns.kakao.token.url=https://kauth.kakao.com/oauth/token

 

 

 

 

 

이후 Spring Boot 서버를 구동시킨 다음 http://localhost:8080/auth/kakao 로 들어가면 다음과 같이 카카오 소셜 로그인이 잘 되는것을 확인할 수 있다. 😄

 

콘솔 화면

 

카카오 소셜 로그인 구현 완료