Nick Dev

[SPURT] Kakao OIDC로 소셜 로그인 구현하기 - 2편 본문

SPURT

[SPURT] Kakao OIDC로 소셜 로그인 구현하기 - 2편

Nick99 2025. 2. 24. 22:47
반응형

기술 스택 : Kotlin + Spring Boot + Spring Security(OAuth에 적용 X)

목차

  1. 카카오 OIDC 소셜 로그인 구현하기(이론편)
  2. 인증 서버로 요청 보내는 client 선택하기 (Feign VS RestTemplate VS RestClient VS WebClient)
  3. 카카오 OIDC 소셜 로그인 구현하기(구현편)
  4. JWT 기반 인증 구현하기

Overview

  • OAuth를 구현하기 위해서는 Provider(Kakao, Google, ...) 등의 인증 서버에 요청을 보내야 된다!
  • Spring에서는 외부 API 호출할 Client가 여러 종류가 존재한다.
  • 종류 별로 간단한 비교 및 Feign Client를 선택한 이유에 대해 말하려고 한다.

Client 종류 4가지

  1. RestTemplate
  2. RestClient
  3. WebClient
  4. Feign Client

모두 동일한 로직으로 예시 코드를 작성했다.

  • getToken()은 인가 코드(authCode)로 리소스 서버의 액세스 토큰 및 OIDC 토큰을 요청하는 함수다.
  • getPublicKeys는 OIDC 토큰을 parsing할 공개 키 목록을 카카오에 요청하는 함수다.

코드의 길이나 가독성을 비교해보면 좋을 것 같다.

1. RestTemplate

RestTemplate은 가장 전통적인 동기적 호출 방식이다.

최근에 주석에 deprecated 된다고 적혀 있었다가 maintenance mode로 수정되었다.
유지보수 모드이고, 보일러 플레이트 코드가 많고 복잡(?)하기에 후보에서 제외했다.

예시 코드

@Component
class KakaoRestTemplateClient(
    private val restTemplate: RestTemplate,
    @Value("\${kakao.auth.url}") private val baseUrl: String
) {
    fun getToken(
        clientId: String,
        redirectUri: String,
        code: String,
        clientSecret: String,
        grantType: String = "authorization_code"
    ): OAuthTokenResponse {
        val params = LinkedMultiValueMap<String, String>().apply {
            add("grant_type", grantType)
            add("client_id", clientId)
            add("redirect_uri", redirectUri)
            add("code", code)
            add("client_secret", clientSecret)
        }

        return restTemplate.postForObject(
            "$baseUrl/oauth/token",
            HttpEntity(params, HttpHeaders().apply {
                contentType = MediaType.APPLICATION_FORM_URLENCODED
            }),
            OAuthTokenResponse::class.java
        ) ?: throw RuntimeException("Failed to get token from Kakao")
    }

    @Cacheable(value = [Cache.OIDC_PUBLIC_KEYS], key = "'kakao'")
    fun getPublicKeys(): OIDCPublicKeyList {
        return restTemplate.getForObject(
            "$baseUrl/.well-known/jwks.json",
            OIDCPublicKeyList::class.java
        ) ?: throw RuntimeException("Failed to get public keys from Kakao")
    }
}

2. RestClient

Spring 6.1에서 새로 도입된 동기식 호출 방식이다.
RestTemplate의 현대적 대안으로 더 유연하고 직관적인 방식이다.
하지만 이후에 소개할 Feign Client에 비하면.. 아직 갈 길이 멀다..

예시 코드

@Component
class KakaoRestClient(
    @Value("\${kakao.auth.url}") private val baseUrl: String
) {
    private val client = RestClient.builder()
        .baseUrl(baseUrl)
        .build()

    fun getToken(
        clientId: String,
        redirectUri: String,
        code: String,
        clientSecret: String,
        grantType: String = "authorization_code"
    ): OAuthTokenResponse {
        return client.post()
            .uri("/oauth/token")
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .body(mapOf(
                "grant_type" to grantType,
                "client_id" to clientId,
                "redirect_uri" to redirectUri,
                "code" to code,
                "client_secret" to clientSecret
            ))
            .retrieve()
            .body(OAuthTokenResponse::class.java)
            ?: throw RuntimeException("Failed to get token from Kakao")
    }

    @Cacheable(value = [Cache.OIDC_PUBLIC_KEYS], key = "'kakao'")
    fun getPublicKeys(): OIDCPublicKeyList {
        return client.get()
            .uri("/.well-known/jwks.json")
            .retrieve()
            .body(OIDCPublicKeyList::class.java)
            ?: throw RuntimeException("Failed to get public keys from Kakao")
    }
}

3. WebClient

비동기/리액티브를 지원하는 client다.
하지만 OAuth 통신에서는 비동기적인 처리가 필요없다.
또한, 현재 프로젝트는 Spring MVC 구조이기에 WebClient의 장점인 비동기 요청의 장점을 활용하지 못한다.
(이유는 MVC는 Thread-per-Request 구조이기에 비동기로 요청을 보내도 서블릿 스레드가 응답을 반환하려면 비동기 요청의 응답을 받아야 되기 때문에 결국 기다려야 함)

예시 코드

@Component
class KakaoWebClient(
    @Value("\${kakao.auth.url}") private val baseUrl: String
) {
    private val webClient = WebClient.builder()
        .baseUrl(baseUrl)
        .build()

    fun getToken(
        clientId: String,
        redirectUri: String,
        code: String,
        clientSecret: String,
        grantType: String = "authorization_code"
    ): OAuthTokenResponse {
        return webClient.post()
            .uri("/oauth/token")
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .body(BodyInserters.fromFormData(
                "grant_type", grantType)
                .with("client_id", clientId)
                .with("redirect_uri", redirectUri)
                .with("code", code)
                .with("client_secret", clientSecret)
            )
            .retrieve()
            .bodyToMono(OAuthTokenResponse::class.java)
            .block() ?: throw RuntimeException("Failed to get token from Kakao")
    }

    @Cacheable(value = [Cache.OIDC_PUBLIC_KEYS], key = "'kakao'")
    fun getPublicKeys(): OIDCPublicKeyList {
        return webClient.get()
            .uri("/.well-known/jwks.json")
            .retrieve()
            .bodyToMono(OIDCPublicKeyList::class.java)
            .block() ?: throw RuntimeException("Failed to get public keys from Kakao")
    }
}

4. Feign Client

Spring Cloud Fegin은 본래 목적은 MSA 환경에서 서비스 간 통신을 더 쉽게 만들기 위해 개발된 것이다.
하지만 선언적이고 직관적인 API 인터페이스 덕분에 MSA 환경 뿐만 아니라 다양한 외부 API 호출에도 사용되고 있다.

장점

  • Spring MVC 어노테이션(ex. @GetMapping, @RequestParam)들을 그대로 사용할 수 있다.
  • interface만으로 API 호출 코드를 작성할 수 있다는 장점이 있다.
  • 앞서 볼 수 있듯이 URL 조합, 파라미터 설정, 응답 파싱 등의 보일러 플레이트 코드를 제거할 수 있다!! (구현체가 런타임에 자동으로 해줌)

예시 코드

@Component
@FeignClient(
    name = "kakaoAuthClient",
    url = "https://kauth.kakao.com",
)
interface KakaoFeignClient {
    /**
     * 인가 코드로 카카오 인증 서버에 ID token 요청하기
     */
    @PostMapping("/oauth/token")
    fun getToken(
        @RequestParam("grant_type") grantType: String = "authorization_code",
        @RequestParam("client_id") clientId: String,
        @RequestParam("redirect_uri") redirectUri: String,
        @RequestParam("code") code: String,
        @RequestParam("client_secret") clientSecret: String,
    ): OAuthTokenResponse

    /**
     * 카카오 인증 서버가 ID 토큰 서명 시 사용한 공개키 목록을 조회
     * 조회 결과 Redis에 캐시
     */
    @GetMapping("/.well-known/jwks.json")
    @Cacheable(value = [Cache.OIDC_PUBLIC_KEYS], key = "'kakao'")
    fun getPublicKeys(): OIDCPublicKeyList
}

결론

@RequestParam, @PostMapping 등 Spring의 친숙한 어노테이션을 그대로 사용하고, 인터페이스 방식으로 직관적이고 깔끔하게 정의할 수 있어서 Fegin Client 선택

반응형