OIDC
간단하게 요약하면, 기존 oauth2에서 액세스토큰을 받으면 유효한 사용자인지 인증하기 위해서 한번더 요청을 보냈는데, 이 과정을 줄이고자 사용자인증기능을 위한 JWT 토큰을 액세스토큰과 함께 보내 내부 검증할 수 있게 만든 구조입니다.
카카오 로그인에서는 ‘id_token’ 이 이에 해당됩니다.
공개키 ID는 헤더의 ‘kid’ 필드, 키(key id) 에 해당됩니다. 우리는 이제 공개키목록에서 kid에 해당하는 공개키 값을 확인하고 그 값으로 ‘서명’ 부분을 검증하면 됩니다.
public class OidcIdTokenParser {
public Claims parse(Locator<Key> locator, String issuer, String token) {
try {
JwtParser jwtParser = Jwts.parser()
.keyLocator(locator)
.requireIssuer(issuer)
.clock(() -> Date.from(Clock.systemUTC().instant()))
.build();
return jwtParser.parseSignedClaims(token).getPayload();
} catch (Exception e) {
log.error("OIDC id_token 파싱 에러 = {}, origin={}", e.getMessage(), "OidcIdTokenParser.parse");
throw new SocialLoginException();
}
}
}
파싱을 keyLocator라는 메서드로 진행한 모습입니다.
서버 내에서 사용하는 jwt를 verifyWith 메서드로 파싱해왔었는데 여기서는 그럴 수 없습니다. 동적으로 키를 찾아야 하기 때문입니다.
package io.jsonwebtoken;
public interface Locator<T> {
T locate(Header var1);
}
public class KakaoJwkLocator implements Locator<Key> {
private final KakaoOidcJwkSetProvider kakaoOidcJwkSetProvider;
private final OidcPublicKeyProvider oidcPublicKeyProvider;
@Override
public Key locate(Header header) {
String publicKeyId = (String) header.get("kid");
if (publicKeyId == null) {
log.error("공개키가 존재하지 않음, origin={}", "KakaoJwkLocator.locate");
throw new SocialLoginException();
}
return oidcPublicKeyProvider.get(publicKeyId, kakaoOidcJwkSetProvider);
}
}
keyLocator에 넘길 구현체입니다.
매개변수로 Header를 받고 원하는 키 타입 T 을 리턴하는 인터페이스네요.
어떤 키가 들어있을 줄 모르기 때문에 Key 또한 인터페이스로 리턴해주겠습니다.
public class KakaoOidcJwkSetProvider implements Supplier<JwkSet> {
@Override
public JwkSet get() {
RestClient restClient = RestClient.create("https://kauth.kakao.com/.well-known/jwks.json");
String response = restClient.get().retrieve().body(String.class);
Parser<JwkSet> jwkParser = Jwks.setParser().build();
return jwkParser.parse(response);
}
}
해당 클래스에서는 카카오 jwks 엔드포인트에 HTTP 요청을 보내 공개키 목록들을 가져오고 파싱합니다.
그러나 이 클래스를 Locator에서 바로 호출하면 안됩니다.

https://developers.kakao.com/docs/latest/ko/kakaologin/utilize#oidc-id-token-verify
4번 항목
- 공개 키를 일정 기간 캐싱하여 사용할 것을 권장하며
- 지나친 요청은 차단될 수 있음을 유의
너무 많은 API 요청을 거부하겠다는 뜻인 것 같습니다. 사실 생각해보면 매 요청마다 공개키목록을 조회하는 API 를 보내면 결국에는 Oauth2에서 액세스토큰을 검증하는 요청을 줄여보자는 의의가 퇴색되기도 합니다.
다시 돌아와서 우리는 공개키를 캐싱해야합니다.
public class OidcPublicKeyProvider {
private final Map<String, Key> jwks = new ConcurrentHashMap<>();
private final ReentrantLock lock = new ReentrantLock();
public Key get(String id, Supplier<JwkSet> supplier) {
Key key = jwks.get(id);
if (key != null) return key;
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
key = jwks.get(id);
if (key != null) return key;
JwkSet jwkSet = supplier.get();
jwkSet.forEach(jwk -> jwks.put(jwk.getId(), jwk.toKey()));
return jwks.get(id);
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
throw new IllegalArgumentException();
}
}
public interface Supplier<T> {
T get();
}
kid에 해당하는 Key가 없을 때만 캐시를 갱신하도록했습니다.
동시성을 고려해 락을 사용하고 락을 기다리는 동안 다른 스레드에서 캐시를 갱신했을 가능성을 고려해 다시한번 캐시를 확인한 모습입니다.
이런 설계 덕분에 OidcPublicKeyProvider 는 어떤 로그인 플랫폼과 관계없이 일관되게 공개키를 캐싱하고 리턴해줄 수 있게 되었습니다.
참고
'개념잡기' 카테고리의 다른 글
| 가비지 컬렉션 (1) | 2025.07.16 |
|---|---|
| 웹 공격으로 생각해보는 JWT (0) | 2025.06.02 |
| 캐시 (1) | 2025.05.28 |