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시간을 주면 됩니다.