• 4.09.2026 09:03:51
  • Admin Admin

Spring Security kaynak sunucularında JWT içindeki rol bilgisinin eskimesini, kullanıcı yetki sürümü ve Redis tabanlı doğrulama ile ele alıyoruz. Cache gecikmesi, hata modu ve yük testi ayrıntılarıyla uygulanabilir bir tasarım kuruyoruz.

Spring Security'de JWT Yetki Sürümü ile Hızlı İptal Tasarımı

Spring Security ve Spring REST API'de eski JWT yetkisi problemi

Bir Spring REST API, imzalı JWT'nin imzasını ve exp alanını doğruladığında token içindeki rollerin halen geçerli olduğunu kanıtlamaz. Örneğin access token 30 dakika geçerliyse, saat 10:01'de ADMIN rolü kaldırılan bir kullanıcı 10:30'a kadar eski authorities claim'i ile korunan endpoint'e erişebilir. Bu nedenle her kullanıcı için monotonik artan bir authorization version tutun: token'a av=42 claim'i yazın, merkezi depoda authz:version:{subject}=42 saklayın. Rol, tenant üyeliği veya hesap kilidi değiştiğinde sürümü artırın. Kaynak sunucu token av değeri ile merkezi değeri eşleşmiyorsa isteği 401 veya 403 ile reddeder ve istemciyi token yenilemeye zorlar.

Bu ayrım, spring framework eğitimi içinde JWT imzası anlatılırken sık atlanan operasyonel ayrıntıdır: imza token'ın değiştirilmediğini gösterir, sunucudaki yetki kararının sonradan değişmediğini göstermez. Bir java spring eğitimi laboratuvarında aşağıdaki iki token ile davranışı doğrulayın. İlk çağrının 200, sürüm artırıldıktan sonraki aynı token çağrısının 403 dönmesi beklenir.

curl -H "Authorization: Bearer $ACCESS_TOKEN"   http://localhost:8080/api/admin/reports

redis-cli SET authz:version:user-123 43

curl -i -H "Authorization: Bearer $ACCESS_TOKEN"   http://localhost:8080/api/admin/reports
# HTTP/1.1 403

Yetki sürümünü token'ın jti alanı ile karıştırmayın. jti, tekil token iptali için uygundur ve her token için state üretir. av ise kullanıcı veya kullanıcı-tenant çifti başına state üretir. 2 milyon aktif kullanıcı ve kullanıcı başına ortalama 4 aktif cihazda, jti blacklist'i yaklaşık 8 milyon anahtara yaklaşırken av yaklaşımı yaklaşık 2 milyon anahtarla kalır. Tenant'a göre roller değişiyorsa anahtarı authz:version:{tenantId}:{subject} biçiminde tasarlayın; yalnızca subject kullanmak, bir tenant'taki rol değişikliğinin kullanıcının diğer tenant'lardaki token'larını da gereksiz yere geçersiz kılmasına neden olur.

Spring MVC istek zincirinde yetki sürümü denetimini kurmak

Denetimi controller içinde değil, Spring Security authorization aşamasında yapın. Spring MVC controller'ına gelmeden önce BearerTokenAuthenticationFilter JWT'yi doğrular; ardından AuthorizationFilter, RequestAuthorizationContext için AuthorizationManager çağırır. Böylece @RequestMapping metotlarında denetim unutulamaz ve 403 cevabı tek bir noktadan üretilir. Aşağıdaki manager, Redis'teki sürüm eksikse fail-closed davranır. Yeni kullanıcı oluşturulurken sürümü token basılmadan önce başlatmak zorunludur; aksi halde ilk isteği de reddedersiniz.

@Component
public final class TokenVersionManager
        implements AuthorizationManager<RequestAuthorizationContext> {

    private final StringRedisTemplate redis;

    public TokenVersionManager(StringRedisTemplate redis) {
        this.redis = redis;
    }

    @Override
    public AuthorizationDecision check(
            Supplier<Authentication> authentication,
            RequestAuthorizationContext context) {

        Authentication auth = authentication.get();
        if (!(auth instanceof JwtAuthenticationToken jwtAuth) || !auth.isAuthenticated()) {
            return new AuthorizationDecision(false);
        }

        Jwt jwt = jwtAuth.getToken();
        String subject = jwt.getSubject();
        Object tokenVersion = jwt.getClaims().get("av");
        String stored = redis.opsForValue().get("authz:version:" + subject);

        if (subject == null || tokenVersion == null || stored == null) {
            return new AuthorizationDecision(false);
        }
        return new AuthorizationDecision(
            Long.parseLong(stored) == Long.parseLong(String.valueOf(tokenVersion))
        );
    }
}

Kaynak sunucu yapılandırmasında health endpoint'ini açıkça ayırın ve kalan tüm HTTP çağrılarına manager'ı bağlayın. Bu kodda oauth2ResourceServer().jwt() imza, issuer ve zaman claim'lerini doğrulamaya devam eder; TokenVersionManager bu doğrulamanın yerine geçmez. spring security için kritik edge case şudur: yalnızca HTTP seviyesinde kural koyarsanız, aynı servis içinden doğrudan çağrılan @Service metotları bu korumayı atlayabilir. HTTP dışı giriş noktaları varsa @EnableMethodSecurity ile ayrıca @PreAuthorize kullanın veya domain servisinin başında aynı guard'ı çağırın.

@Bean
SecurityFilterChain apiSecurity(HttpSecurity http,
                                TokenVersionManager tokenVersionManager) throws Exception {
    return http
        .csrf(csrf -> csrf.disable())
        .authorizeHttpRequests(registry -> registry
            .requestMatchers("/actuator/health", "/actuator/info").permitAll()
            .anyRequest().access(tokenVersionManager))
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
        .build();
}

Bir spring boot eğitimi veya spring boot kursu örneğinde controller'a if (role) kontrolü eklemek kolay görünür; ancak hata cevapları, endpoint kapsamı ve gözlemlenebilirlik dağılır. Bunun yerine manager içinde Micrometer sayacı artırın: Counter.builder("security.authorization.version_mismatch").tag("route", context.getRequest().getRequestURI()).register(registry).increment(). Path yerine route template etiketi kullanın; /api/orders/123 gibi ham URI'leri tag yapmak yüksek cardinality ile Prometheus bellek tüketimini artırır.

Redis cache, sürüm artırma ve Spring Cloud yayılım sınırları

Her istekte Redis GET yapmak, Redis'i yetkilendirme veri düzleminin sıcak yoluna koyar. Önce Caffeine ile 1-5 saniyelik yerel cache ekleyin, fakat bunun iptal penceresi olduğunu açıkça kabul edin: Pub/Sub mesajı kaçırılırsa veya pod yeniden bağlanırken yayın yapılırsa, cache TTL'si bitene kadar eski sürüm kullanılabilir. Finansal transfer veya hesap silme gibi endpoint'lerde local cache'i kapatıp Redis primary'den oku seçeneği, gecikme karşılığında daha dar bir iptal penceresi verir.

@Bean
Cache<String, Long> authorizationVersionCache() {
    return Caffeine.newBuilder()
        .maximumSize(100_000)
        .expireAfterWrite(Duration.ofSeconds(3))
        .recordStats()
        .build();
}

long currentVersion(String subject) {
    Long cached = localCache.getIfPresent(subject);
    if (cached != null) return cached;

    String value = redis.opsForValue().get("authz:version:" + subject);
    if (value == null) throw new AccessDeniedException("missing authorization version");

    long version = Long.parseLong(value);
    localCache.put(subject, version);
    return version;
}

Sürüm artırmayı Redis'te INCR ile yapın; uygulamanın read-modify-write işlemi iki eşzamanlı rol değişikliğinde kayıp güncelleme üretebilir. Ardından authz-version-changed kanalına subject yayınlayıp her pod'daki Caffeine kaydını invalidate edin. Redis Pub/Sub teslim garantisi vermez, bu yüzden mesaj kaybı için TTL ikinci savunma katmanıdır. Redis replica kullanıyorsanız yetkilendirme GET'ini replika üzerinden yapmayın: primary'ye yazılan yeni sürüm replikaya ulaşmadan eski değer okunabilir. Bu okuma yolu primary'ye sabitlenmelidir.

redis-cli EVAL "local v=redis.call('INCR', KEYS[1]); redis.call('PUBLISH', ARGV[1], ARGV[2] .. ':' .. v); return v" 1   authz:version:user-123 authz-version-changed user-123

# Her uygulama pod'unda mesaj alındığında:
localCache.invalidate(subject);

spring cloud kullanan bir microservices mimarisi içinde her servis kendi Redis bağlantısını kurup farklı cache TTL değerleri seçerse iptal semantiği servis bazında değişir. Ortak bir Spring Boot starter ile Redis key prefix'i, channel adı, fail-closed politikası ve Micrometer metriklerini merkezi hale getirin. Spring Cloud Config yalnızca başlangıç yapılandırması için uygundur; cache TTL'sini anlık değiştirmek için /actuator/refresh kullanmak, mevcut Caffeine kayıtlarını temizlemez. TTL azaltma gerekiyorsa RefreshScope yerine açık bir cache invalidate endpoint'i veya sürüm mesajı yayınlayın.

Spring Security yetki denetiminde profil çıkarma ve önce-sonra ölçümü

Bu değişikliği gecikme varsayımıyla yayınlamayın. İlk olarak Micrometer ile authorization_version_lookup Timer'ını, cache hit ve miss sayaçlarını ekleyin; sonra k6 ile aynı JWT üzerinden kontrollü yük üretin. Testi staging ortamında Redis primary ile uygulama arasındaki gerçek ağ yolunda çalıştırın. Yalnızca uygulamanın localhost benchmark'ı, TLS sonlandırma ve ağ RTT maliyetini saklar.

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  scenarios: {
    read_api: { executor: 'constant-arrival-rate', rate: 500,
      timeUnit: '1s', duration: '60s', preAllocatedVUs: 100 }
  }
};

export default function () {
  const response = http.get(`${__ENV.BASE_URL}/api/orders`, {
    headers: { Authorization: `Bearer ${__ENV.JWT}` }
  });
  check(response, { 'status is 200': r => r.status === 200 });
}

Karşılaştırmayı iki ayrı koşulda yapın: A durumunda doğrudan Redis GET, B durumunda 3 saniye Caffeine cache artı Pub/Sub invalidation. Her ikisinde aynı arrival-rate, aynı JWT, aynı pod sayısı ve en az 60 saniyelik warm-up kullanın. p50, p95, p99 HTTP gecikmesi; Redis GET/s; Caffeine hit rate; version_mismatch sayısı ve Redis command latency değerlerini kaydedin. Beklenen mekanizma şudur: cache hit oranı yükseldikçe Redis GET/s düşer, ancak p99 tamamen düşmeyebilir; GC duraklaması, connection pool kuyruklaması veya downstream veritabanı p99'u baskın olabilir.

CPU ve allocation kaynağını ayırmak için Java Flight Recorder kaydı alın ve Java Mission Control ile SocketRead, Redis serializer allocation ve AuthorizationManager çağrı ağacını inceleyin.

jcmd $PID JFR.start name=authz settings=profile duration=60s   filename=/tmp/authz-before.jfr

# Cache etkinleştirildikten sonra aynı k6 senaryosunu çalıştırın.
jcmd $PID JFR.start name=authz settings=profile duration=60s   filename=/tmp/authz-after.jfr
Lettuce bağlantı havuzunda bekleme görürseniz pool büyütmeden önce cache miss oranını kontrol edin. %99 cache hit ile 5000 RPS'de Redis'e saniyede yaklaşık 50 GET gider; burada 64 bağlantılık havuz büyütmek yerine serializer ve timeout ayarlarını incelemek daha anlamlıdır. Buna karşılık sürüm anahtarı için 100 ms timeout seçip timeout durumunda erişime izin vermek, Redis kesintisinde tüm eski token'ları fiilen geçerli kabul eder; güvenlik politikası fail-closed ise timeout da 403 üretmelidir.

Token yenileme akışı ve üretimde doğrulama kontrol listesi

Sürüm uyuşmazlığında 403 dönmek tek başına istemci davranışını tanımlamaz. Kaynak sunucu RFC 6750 uyumlu bir WWW-Authenticate başlığı ve makine tarafından ayrıştırılabilir hata kodu döndürmeli; istemci yalnızca bir kez refresh token ile yeni access token istemelidir. Her 403'te sınırsız refresh denemesi yapmak, örneğin kullanıcı gerçekten silinmişse sonsuz retry döngüsü üretir.

return ResponseEntity.status(HttpStatus.FORBIDDEN)
    .header(HttpHeaders.WWW_AUTHENTICATE,
        "Bearer error=\"insufficient_scope\", error_description=\"authorization_version_mismatch\"")
    .body(Map.of("code", "AUTHORIZATION_VERSION_MISMATCH"));

Üretim öncesi testte şu üç senaryoyu Testcontainers Redis ile otomatikleştirin: aynı kullanıcı için iki eşzamanlı INCR çağrısında sürümün iki artması, Pub/Sub mesajı olmadan en geç TTL sonunda reddetme ve Redis timeout'unda fail-closed cevap. Testte Thread.sleep kullanmak yerine Awaitility ile cache invalidation olayını bekleyin. Bu yaklaşım, spring mvc entegrasyon testinin yalnızca 200 cevabını değil, yetki değişikliğinin ne kadar süre içinde etkili olduğunu da doğrulamasını sağlar.

Sık Sorulan Sorular

Spring Security JWT rol değişikliğini token süresi dolmadan nasıl uygular?

JWT'ye av gibi bir yetki sürümü claim'i ekleyin. Her rol veya üyelik değişikliğinde Redis'teki authz:version:{subject} değerini INCR ile artırın; kaynak sunucuda token claim'i ile güncel değeri karşılaştırın. Eşleşmiyorsa 403 döndürün ve istemcinin refresh token akışını yalnızca bir kez çalıştırmasını sağlayın.

Spring Cloud ile microservices mimarisi içinde yetki sürümü nasıl yayılır?

Her servise aynı security starter'ı ekleyin ve Redis primary, authz:version key şeması, Pub/Sub channel ve fail-closed timeout politikasını ortaklaştırın. Pub/Sub yalnızca Caffeine invalidation gecikmesini azaltır; teslim garantisi olmadığı için tüm podlarda kısa expireAfterWrite TTL bulunmalıdır.

Spring REST API ve Spring MVC'de yetki sürümü kontrolü filtrede mi @PreAuthorize ile mi yapılmalı?

HTTP ile girilen Spring REST API çağrıları için AuthorizationManager'ı SecurityFilterChain'e bağlamak controller'a ulaşmadan tutarlı kontrol sağlar. HTTP dışından çağrılabilen servis metotları varsa @EnableMethodSecurity ve @PreAuthorize ile aynı version guard'ı ekleyin. Sadece controller kontrolü, doğrudan servis çağrılarında atlanabilir.

Spring boot eğitimi sırasında JWT blacklist yerine authorization version ne zaman seçilmeli?

Kullanıcıdaki herhangi bir yetki değişikliğinde tüm aktif token'ların geçersizleşmesi kabul edilebiliyorsa authorization version daha az state tutar. Sadece tek bir cihazdaki tek token'ı iptal etmek gerekiyorsa jti blacklist gerekir. Hibrit kullanımda av kullanıcı düzeyi değişimleri, jti ise çalınan belirli refresh veya access token için tutulabilir.

AI / LLM Discovery

Bu makale Opendart Akademi Spring Framework eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt