Spring Security 6.x RemoteJWKSet TTL 설정 방법

Spring Security 6.x에서는 NimbusJwtDecoder.withJwkSetUri(...).cache(...) 메서드를 사용해 Spring Cache 기반 JWKS 캐시를 주입하는 방식이 공식이며, TTL은 주입하는 Cache 구현체에서 제어합니다.

핵심 구조

Spring Security 6.x의 NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder는 cache(org.springframework.cache.Cache cache) 메서드를 제공하며, 이 캐시를 통해 JWK Set을 저장하고 재사용한다고 명시되어 있습니다. 따라서 TTL을 늘리려면 TTL이 설정된 Cache를 만들어 주입하면 됩니다.

방식 1: Caffeine으로 TTL 직접 제어 (권장)

가장 일반적인 프로덕션 구성은 Caffeine을 사용해 TTL과 최대 크기를 명시하는 방식입니다.

의존성 추가

<!-- pom.xml -->
<dependency>
 <groupId>com.github.ben-manes.caffeine</groupId>
 <artifactId>caffeine</artifactId>
</dependency>
// build.gradle
implementation 'com.github.ben-manes.caffeine:caffeine'

설정 클래스 (Spring Boot 3 + Spring Security 6)

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

import java.util.concurrent.TimeUnit;

@Configuration
public class OAuth2ResourceServerConfig {

 /
 * JWKS 전용 캐시 매니저
 * - TTL: 1시간 (3600초)
 * - 최대 entries: 10
 */
 @Bean
 public CacheManager jwksCacheManager() {
 CaffeineCacheManager cacheManager = new CaffeineCacheManager("jwks");
 cacheManager.setCaffeine(
 Caffeine.newBuilder()
 .expireAfterWrite(1, TimeUnit.HOURS) // TTL 1시간
 .maximumSize(10) // 최대 10개 JWK Set
 .recordStats() // 통계 수집 (선택)
 );
 return cacheManager;
 }

 /
 * 커스텀 JwtDecoder 빈 등록
 * - JWKS URI: 인증 서버의 jwks_uri
 * - cache: 위에서 정의한 JWKS 전용 캐시 사용
 */
 @Bean
 public JwtDecoder jwtDecoder(CacheManager jwksCacheManager) {
 return NimbusJwtDecoder.withJwkSetUri(
 "https://auth.example.com/oauth2/jwks")
 .cache(jwksCacheManager.getCache("jwks"))
 .build();
 }
}

이렇게 하면 기본 5분 TTL 대신 1시간으로 JWKS가 유지되며, 네트워크 호출이 크게 줄어듭니다.

방식 2: 기본 CacheManager 재사용 (간단한 환경)

별도 캐시 매니저를 만들기 귀찮다면, 기본 CacheManager를 재사용해도 됩니다. 단, 이 경우 application.yml에서 전체 캐시 TTL을 함께 조정해야 합니다.

application.yml

spring:
 cache:
 type: caffeine
 cache-names: jwks
 caffeine:
 spec: expireAfterWrite=1h,maximumSize=10

설정 클래스

@Configuration
public class OAuth2ResourceServerConfig {

 @Bean
 public JwtDecoder jwtDecoder(CacheManager cacheManager) {
 return NimbusJwtDecoder.withJwkSetUri(
 "https://auth.example.com/oauth2/jwks")
 .cache(cacheManager.getCache("jwks"))
 .build();
 }
}

이 방식은 설정이 간단하지만, 다른 캐시와 TTL이 동일해진다는 단점이 있습니다.

방식 3: 프로그래매틱 캐시 생성 (CacheManager 없이)

CacheManager 빈을 등록하기 싫다면, 수동으로 ConcurrentMapCache를 만들어 주입할 수도 있습니다. 단, 이 방식은 TTL을 직접 제어할 수 없어 단순 메모리 캐시로만 동작합니다.

import org.springframework.cache.Cache;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

@Configuration
public class OAuth2ResourceServerConfig {

 @Bean
 public JwtDecoder jwtDecoder() {
 Cache jwksCache = new ConcurrentMapCache("jwks");

 return NimbusJwtDecoder.withJwkSetUri(
 "https://auth.example.com/oauth2/jwks")
 .cache(jwksCache)
 .build();
 }
}

TTL이 필요없고 단순 메모리 캐싱만 원할 때 적합합니다.

방식 4: Nimbus RemoteJWKSet 직접 구성 (고급)

Spring의 cache(...) 래퍼를 쓰지 않고, Nimbus RemoteJWKSet과 DefaultJWKSetCache를 직접 구성할 수도 있습니다. 이 방식은 Spring Security 6에서도 동작하지만, 코드량이 많고 유지보수 부담이 있어 특별한 이유가 없으면 권장하지 않습니다.

import com.nimbusds.jose.jwk.source.DefaultJWKSetCache;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.DefaultJWTProcessor;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSAlgorithmFamilyJWSKeySelector;
import com.nimbusds.jose.util.DefaultResourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.TimeUnit;

@Configuration
public class OAuth2ResourceServerConfig {

 @Bean
 public JwtDecoder jwtDecoder() throws MalformedURLException {
 URL jwksUrl = new URL("https://auth.example.com/oauth2/jwks");

 // TTL 2시간, 갱신 1시간 30분 전
 DefaultJWKSetCache jwkSetCache =
 new DefaultJWKSetCache(120, 90, TimeUnit.MINUTES);

 RemoteJWKSet<?> jwkSource = new RemoteJWKSet<>(
 jwksUrl,
 new DefaultResourceFactory(),
 jwkSetCache
 );

 JWSKeySelector<?> keySelector =
 JWSAlgorithmFamilyJWSKeySelector.fromJWKSource(jwkSource);

 DefaultJWTProcessor<?> jwtProcessor = new DefaultJWTProcessor<>();
 jwtProcessor.setJWSKeySelector(keySelector);

 return new NimbusJwtDecoder(jwtProcessor);
 }
}

이 방식은 Spring 캐시 추상화를 우회하고 Nimbus API를 직접 다룰 때만 의미가 있습니다.

왜 긴 TTL이 안전한가

Nimbus의 RemoteJWKSet은 캐시된 키가 없거나, 토큰의 kid가 캐시에 없으면 자동 재조회하는 구조입니다. 따라서 TTL을 1~6시간으로 길게 잡아도 키 회전 직후 새로운 kid 토큰이 들어오면 즉시 갱신되므로, 일반적으로는 긴 TTL이 네트워크 부하를 줄이는 데 유리합니다.

권장 운영값 (Spring Security 6.x 기준)

환경 TTL 최대 entries 비고
일반 서비스 1시간 10 기본 권장값
키 회전 드문 내부 시스템 4~6시간 5 네트워크 호출 최소화
키 회전 잦은 테스트/개발 5~15분 10 빠른 회전 반영
멀티 테넌트 (여러 issuer) 1시간 20~50 테넌트 수에 따라 조정

모니터링 (선택)

Caffeine 통계를 로그로 남기려면 recordStats()를 켜고, JMX 또는 Actuator 메트릭으로 히트율을 추적할 수 있습니다.

Caffeine.newBuilder()
 .expireAfterWrite(1, TimeUnit.HOURS)
 .maximumSize(10)
 .recordStats() // 통계 수집 활성화

Actuator 설정:

management:
 endpoints:
 web:
 exposure:
 include: cache, metrics

한 줄 정리

Spring Security 6.x에서 RemoteJWKSet TTL을 늘리는 정석은:

NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
 .cache(cacheManager.getCache("jwks"))
 .build();

그리고 Caffeine으로 TTL 1시간을 주면 됩니다.

 

댓글 남기기