Spring Security RemoteJWKSet 캐싱 설정 예제

Spring Security에서 RemoteJWKSet 캐싱 설정을 하려면, 최신 방식은 NimbusJwtDecoder.withJwkSetUri(...).cache(...)에 Spring Cache를 주입하는 것입니다. 구버전이나 세밀한 TTL 제어가 필요하면 Nimbus의 RemoteJWKSet과 DefaultJWKSetCache를 직접 구성할 수도 있습니다.

최신 방식

Spring Security 5.4+에서는 NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder.cache(...) 메서드로 JWK Set 저장용 캐시를 넣을 수 있습니다. 즉, 캐시 TTL은 Spring Cache 구현체에서 정하고, 디코더는 그 캐시를 그대로 사용합니다.

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

Caffeine 예제

실무에서는 TTL 제어가 쉬운 Caffeine을 많이 씁니다. 아래 예시는 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();
 }
}

이 설정을 쓰면 JWT 검증 시마다 JWKS 엔드포인트를 호출하지 않고, 캐시된 JWK Set을 재사용합니다.

application.yml 방식

Boot에서 캐시를 중앙 관리하고 싶다면 application.yml로도 설정할 수 있습니다. 이 경우 cache-names에 jwks를 정의하고, 같은 이름의 캐시를 디코더에 연결하면 됩니다.

spring:
 cache:
 type: caffeine
 cache-names: jwks
 caffeine:
 spec: expireAfterWrite=1h,maximumSize=10
@Bean
JwtDecoder jwtDecoder(CacheManager cacheManager) {
 return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/oauth2/jwks")
 .cache(cacheManager.getCache("jwks"))
 .build();
}

Nimbus 직접 구성

Spring Security 5.2 계열처럼 cache(...) 빌더가 없거나, Nimbus 레벨에서 lifespan과 refreshTime을 직접 제어해야 한다면 RemoteJWKSet을 직접 만들 수 있습니다. Stack Overflow 예시에서는 DefaultJWKSetCache(cacheLifespan, refreshTime, TimeUnit.MINUTES)로 TTL과 refresh 시점을 명시합니다.

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

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

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

 long cacheLifespan = 500;
 long refreshTime = 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);
}

캐싱이 동작하는 방식

Nimbus RemoteJWKSet은 기본적으로 JWK Set을 캐시에 저장하고, 만료되면 다시 가져옵니다. 또 토큰의 kid가 캐시에 없는 경우에는 새 JWKS를 다시 읽어 오는 흐름이 있어, TTL을 길게 잡아도 키 회전을 어느 정도 따라갈 수 있습니다.

운영 팁

프로덕션에서는 보통 15분~1시간 TTL이 무난하고, 키 회전이 드문 내부 서비스면 더 길게 가져가기도 합니다. 다만 첫 요청 지연을 줄이려면 애플리케이션 시작 시 JWKS를 미리 읽어 두는 방식도 검토할 만합니다.

원하시면 다음 답변에서 Spring Boot 3 + Keycloak 기준 전체 SecurityFilterChain 포함 예제로 바로 정리해드릴게요.

댓글 남기기