일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- depromeet
- 코드 트리
- dip
- 코딩테스트
- 운영체제
- 자료구조
- Redis
- 연습문제
- 코드트리
- Scaffold
- C언어
- kakao
- 코딩
- Spring
- c
- Sharding
- AOP
- 부하 테스트
- nGrinder
- Kafka
- OAuth
- flutter
- Kotlin
- java
- pub.dev
- 디프만
- 디프만16기
- exception
- Oidc
- 코딩 테스트
- Today
- Total
Nick Dev
[SPURT] Kakao OIDC로 소셜 로그인 구현하기 - 2편 본문
기술 스택 : Kotlin + Spring Boot + Spring Security(OAuth에 적용 X)
목차
- 카카오 OIDC 소셜 로그인 구현하기(이론편)
- 인증 서버로 요청 보내는 client 선택하기 (Feign VS RestTemplate VS RestClient VS WebClient)
- 카카오 OIDC 소셜 로그인 구현하기(구현편)
- JWT 기반 인증 구현하기
Overview
- OAuth를 구현하기 위해서는 Provider(Kakao, Google, ...) 등의 인증 서버에 요청을 보내야 된다!
- Spring에서는 외부 API 호출할 Client가 여러 종류가 존재한다.
- 종류 별로 간단한 비교 및 Feign Client를 선택한 이유에 대해 말하려고 한다.
Client 종류 4가지
RestTemplate
RestClient
WebClient
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 선택
'SPURT' 카테고리의 다른 글
[SPURT] SPURT를 개발하면서 최적화하려고 했던 부분들 (2) | 2025.04.18 |
---|---|
[SPURT] Docker와 GitHub Actions를 활용한 CI/CD 구축 가이드 (0) | 2025.03.07 |
[SPURT] Kakao OIDC로 소셜 로그인 구현하기 - 4편 (0) | 2025.02.25 |
[SPURT] Kakao OIDC로 소셜 로그인 구현하기 - 3편 (0) | 2025.02.25 |
[SPURT] Kakao OIDC로 소셜 로그인 구현하기 - 1편 (2) | 2025.02.24 |