Spring Security RemoteJWKSet TTL 증가 예제

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시간처럼 조절하면 됩니다.

 

댓글 남기기