Spring Security에서 RemoteJWKSet TTL을 늘리는 가장 쉬운 방법은 NimbusJwtDecoder.withJwkSetUri(...).cache(...)를 사용해 Spring Cache를 주입하는 것입니다. 이 방식은 Spring Security 5.4부터 공식적으로 지원되며, JWK Set 저장용 캐시를 직접 지정할 수 있습니다.
가장 간단한 방식
Spring 문서의 NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder에는 cache(org.springframework.cache.Cache cache) 메서드가 있으며, 이 캐시를 통해 JWK Set을 저장할 수 있다고 명시되어 있습니다. 따라서 TTL을 늘리고 싶다면 TTL이 설정된 Cache 구현체를 만들어 주입하면 됩니다.
@Bean
JwtDecoder jwtDecoder(CacheManager cacheManager) {
return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/oauth2/jwks")
.cache(cacheManager.getCache("jwks"))
.build();
}
Caffeine으로 TTL 1시간 예제
실무에서는 ConcurrentMapCache보다 Caffeine 같은 TTL 지원 캐시가 더 적합합니다. 아래 예시는 JWK Set 캐시를 1시간 유지하도록 설정한 예제입니다.
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 SecurityConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("jwks");
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.maximumSize(10));
return cacheManager;
}
@Bean
public JwtDecoder jwtDecoder(CacheManager cacheManager) {
return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/oauth2/jwks")
.cache(cacheManager.getCache("jwks"))
.build();
}
}
이렇게 하면 기본 5분보다 훨씬 긴 TTL로 JWKS가 유지되고, 매 검증마다 원격 JWK 엔드포인트를 호출하지 않게 됩니다.
왜 이 방식이 안전한가
Nimbus의 RemoteJWKSet은 기본적으로 캐시를 사용하며, 오래된 키가 캐시에 있더라도 새 토큰의 kid가 캐시에 없으면 JWKS를 다시 조회하는 구조를 가지고 있습니다. 즉, TTL을 길게 잡아도 알 수 없는 키 ID가 등장하면 자동 갱신되기 때문에, 보통은 긴 TTL이 네트워크 부하를 줄이는 데 유리합니다.
Spring 5.2 계열 직접 구성 예제
Spring Security 5.2에서는 cache(...) 빌더 메서드가 없어서, Stack Overflow 예시처럼 Nimbus RemoteJWKSet과 DefaultJWKSetCache를 직접 구성해야 합니다. 이 방식은 cacheLifespan과 refreshTime을 직접 분 단위로 지정할 수 있습니다.
@Bean
public JwtDecoder jwtDecoder() throws MalformedURLException, KeySourceException {
URL jwksUrl = new URL("https://auth.example.com/oauth2/jwks");
long cacheLifespan = 500; // 500분
long refreshTime = 400; // 400분
JWKSetCache jwkSetCache =
new DefaultJWKSetCache(cacheLifespan, refreshTime, TimeUnit.MINUTES);
RemoteJWKSet<SecurityContext> jwkSet =
new RemoteJWKSet<>(jwksUrl, null, jwkSetCache);
JWSKeySelector<SecurityContext> keySelector =
JWSAlgorithmFamilyJWSKeySelector.fromJWKSource(jwkSet);
DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(keySelector);
return new NimbusJwtDecoder(jwtProcessor);
}
이 코드는 오래된 Spring 버전에서 RemoteJWKSet의 TTL을 직접 늘리는 정석적인 우회 방법으로 소개됩니다.
Boot 설정과 함께 쓰는 형태
Spring Boot에서 issuer 기반 검증을 쓰고 있더라도, TTL 제어가 필요하면 spring.security.oauth2.resourceserver.jwt.issuer-uri만 두지 말고 커스텀 JwtDecoder 빈을 직접 등록하는 편이 낫습니다. 그래야 기본 내부 디코더 대신 여러분이 정의한 캐시 전략이 적용됩니다.
추가 최적화 팁
TTL을 길게 잡을 때는 RestOperations도 함께 커스터마이징하는 것이 좋습니다. Spring 빌더는 restOperations(...)도 지원하므로 연결 타임아웃, 읽기 타임아웃, 프록시 설정을 따로 줄 수 있습니다. 예를 들어 JWKS 서버가 느린 환경이라면 RestTemplate에 짧은 timeout을 걸어 장애 전파를 줄일 수 있습니다.
@Bean
public JwtDecoder jwtDecoder(CacheManager cacheManager) {
RestTemplate restTemplate = new RestTemplate();
// 필요 시 requestFactory로 connect/read timeout 지정
return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/oauth2/jwks")
.restOperations(restTemplate)
.cache(cacheManager.getCache("jwks"))
.build();
}
권장 운영값
실무에서는 보통 아래처럼 잡습니다:
- 15분~1시간 TTL: 일반 서비스
- 수시간 TTL: 키 회전이 드문 내부 시스템
- 5분 이하 TTL: 회전이 잦거나 테스트 환경
너무 짧으면 원격 호출이 많아지고, 너무 길면 정상 키 회전 반영이 늦어질 수 있지만, Nimbus는 unknown kid 재조회가 가능하므로 일반적으로는 긴 TTL이 더 경제적입니다.
한 줄 예제 정리
가장 추천하는 최신 방식은 아래 조합입니다:
NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.cache(cacheManager.getCache("jwks"))
.build();
그리고 캐시 쪽에서 TTL을 1시간, 6시간처럼 조절하면 됩니다.