Spring Security ve Open Policy Agent (OPA) ile merkezi yetkilendirme kararları kurmayı; Spring MVC entegrasyonunu, gecikme ölçümünü, politika sürümlemeyi ve güvenli karar önbelleğini ele alır.
Spring Security’de OPA ile Politika Tabanlı Yetkilendirme Tasarımı
Spring Security ve microservices mimarisi için karar sınırını kurmak
Dağıtık bir microservices mimarisi içinde her servisin JWT claim’lerinden kendi rol matrisini üretmesi, politika değişikliğinde çok sayıda bağımsız dağıtım gerektirir. Bunun yerine kimlik doğrulamayı Spring Security’de, işleme özgü yetkilendirme kararını OPA’da tutun. OPA’ya yalnızca karar için gereken bağlamı gönderin: doğrulanmış özne kimliği, tenant, HTTP metodu, normalize edilmiş kaynak ve alan seviyesindeki sahiplik verisi. JWT’nin tamamını iletmek gereksiz kişisel veri yayar ve politika dilini identity-provider claim şemasına bağlar.
Örneğin kaynak kimliği path’ten alınırken URL decode işlemini yalnızca bir kez yapın; aksi halde `%2F` ve çift encode edilmiş değerler politika ile denetleyici arasında farklı kaynaklara dönüşebilir. Spring MVC tarafında eşleşmiş route bilgisini `HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE` üzerinden, somut kimliği ise path variable’dan alın. Bu ayrım, `/orders/{orderId}` için politika yazarken `/orders/42` gibi sınırsız path cardinality’sini OPA karar metriklerine taşımayı engeller.
@Component
final class OrderAuthorizationManager
implements AuthorizationManager<RequestAuthorizationContext> {
private final OpaClient opa;
@Override
public AuthorizationDecision check(Supplier<Authentication> auth,
RequestAuthorizationContext ctx) {
Authentication a = auth.get();
String orderId = ctx.getVariables().get("orderId");
OpaInput input = new OpaInput(
a.getName(), tenant(a), "order:read", orderId,
ctx.getRequest().getMethod(), policyRevision());
return new AuthorizationDecision(opa.allow(input));
}
}
@Bean
SecurityFilterChain api(HttpSecurity http,
OrderAuthorizationManager orders) throws Exception {
return http.authorizeHttpRequests(registry -> registry
.requestMatchers(HttpMethod.GET, "/orders/{orderId}").access(orders)
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt())
.build();
}Buradaki kritik ayrıntı `AuthorizationManager` çağrısının filtre zincirinde, controller çalışmadan önce yapılmasıdır. `@PreAuthorize` da kullanılabilir; ancak `@PreAuthorize` proxy tabanlı olduğundan aynı bean içindeki self-invocation çağrılarında devreye girmez. HTTP kaynak yetkilendirmesi için `authorizeHttpRequests` ile merkezî kural, servis katmanında gerçekten ayrı iş operasyonları için method security kullanmak daha öngörülebilir bir sınır oluşturur.
Spring Boot eğitimi bağlamında OPA policy paketini sürümlemek
Bir spring boot eğitimi projesinde politikayı yalnızca `allow := true` örneğiyle göstermek gerçek sistem davranışını gizler. Her istekte `policy_revision` gönderin ve OPA bundle manifest’indeki revizyonla ilişkilendirin. Böylece audit kaydı, bir iznin hangi policy commit’iyle verildiğini taşır. OPA bundle’ını CI aşamasında `opa fmt`, `opa check` ve unit test ile doğrulayın; başarısız testte bundle yayınlamayın.
package authz.orders
default allow := false
allow if {
input.action == "order:read"
input.subject.tenant == input.resource.tenant
input.subject.id == input.resource.owner_id
}
allow if {
input.action == "order:read"
"support" in input.subject.roles
input.subject.tenant == input.resource.tenant
}Bu örnekte `owner_id` değerini istemciden kabul etmeyin. Java servisi, order kaydından veya güvenilir bir domain read-model’inden üretmelidir; aksi halde saldırgan `resource.owner_id` alanını kendi kimliğiyle gönderip Rego koşulunu geçebilir. Ayrıca tenant izolasyonunu yalnızca route prefix’ine bırakmayın: politika içinde hem subject hem resource tenant’ını karşılaştırmak, yanlış yönlendirilmiş bir Spring Cloud çağrısında ikinci savunma katmanı sağlar.
Bundle testini somutlaştırmak için CI’da şu komutları çalıştırın:
opa fmt --fail policies/
opa check policies/
opa test policies/ -v
opa build -b policies/ -o bundle.tar.gz `opa test` içine tenant uyuşmazlığı, boş rol listesi, destek kullanıcısının başka tenant’a erişimi ve action typo durumlarını ekleyin. Rego’da tanımsız alanların çoğu zaman false’a düşmesi, schema değişiminde sessiz yetki kesintisi üretebilir; bu nedenle OPA’nın JSON Schema doğrulamasını veya giriş şemasını CI testleriyle zorunlu hale getirin.Spring REST API gecikmesini ölçerek karar önbelleği eklemek
OPA sidecar çağrısı ağ geçişi ve JSON serileştirmesi eklediği için, önbellek eklemeden önce spring rest api yetkilendirme maliyetini ölçün. Spring Boot Actuator ve Micrometer ile yalnızca OPA kararını zamanlayın; genel `http.server.requests` metriği controller, JDBC ve serialization sürelerini de içerdiğinden kök nedeni ayıramaz. Prometheus histogramı ile p50, p95 ve p99 karşılaştırın; yükü sabit oranlı allow/deny istekleriyle k6 üzerinden üretin.
@Bean
RestClient opaRestClient(RestClient.Builder builder) {
return builder.baseUrl("http://127.0.0.1:8181")
.requestInterceptor(new OpaTimingInterceptor(MeterRegistryHolder.registry()))
.build();
}
final class OpaTimingInterceptor implements ClientHttpRequestInterceptor {
private final Timer timer;
OpaTimingInterceptor(MeterRegistry registry) {
this.timer = Timer.builder("authz.opa.decision")
.publishPercentileHistogram()
.tag("policy", "orders")
.register(registry);
}
public ClientHttpResponse intercept(HttpRequest req, byte[] body,
ClientHttpRequestExecution next) throws IOException {
return timer.recordCallable(() -> next.execute(req, body));
}
}Önce 10 dakika boyunca önbelleksiz durumda `authz_opa_decision_seconds_bucket` ve `http_server_requests_seconds_bucket` histogramlarını kaydedin. Ardından yalnızca tekrarlanan, salt-okunur kararlar için Caffeine ekleyip aynı k6 senaryosunu, aynı pod sayısı ve aynı OPA bundle’ı ile tekrarlayın. Karar anahtarına `subjectId`, `tenantId`, action, resourceId ve `policyRevision` koyun; sadece kullanıcı kimliğiyle anahtarlamak, aynı kullanıcının farklı order’lara erişim sonucunu yanlışlıkla paylaşır.
Cache<DecisionKey, Boolean> decisions = Caffeine.newBuilder()
.maximumSize(100_000)
.expireAfterWrite(Duration.ofSeconds(3))
.build();
boolean allow(OpaInput in) {
var key = new DecisionKey(in.subjectId(), in.tenantId(), in.action(),
in.resourceId(), in.policyRevision());
return decisions.get(key, ignored -> remoteOpaDecision(in));
}Örnek bir kabul kriteri belirleyin: cache eklemesi sonrası p95 `authz.opa.decision` süresi düşerken deny oranı, 403 sayısı ve policy revision etiketi değişmemelidir. Bu değerleri evrensel hedef gibi değil, kendi başlangıç ölçümünüze karşı kıyas olarak kullanın. `expireAfterWrite` süresini dakikalara çıkarmak risklidir: üyelik kaldırma veya kaynak sahipliği değişiminde eski allow kararı TTL bitene kadar kalır. Yazma, üyelik ve rol değiştirme akışlarında ilgili subject/tenant anahtarlarını aktif olarak invalidate etmek daha güvenlidir.
Spring Cloud çağrılarında kimlik ve politika bağlamını taşımak
spring cloud ile servisler arası çağrıda uç kullanıcı access token’ını her downstream servise iletmek, token audience kapsamını büyütür ve servislerin kullanıcı adına çağrı yapmasını kolaylaştırır. Kullanıcı bağlamı gerçekten gerekiyorsa OAuth 2.0 token exchange veya dar audience’lı token kullanın; yalnızca teknik çağrılar için client-credentials token ve imzalı, kısa ömürlü bir `X-Authorization-Context` kullanın. Downstream servis bu başlığı doğrudan güvenmemeli, gateway veya çağıran servis tarafından oluşturulduğunu mTLS ya da imza ile doğrulamalıdır.
Spring Cloud OpenFeign kullanılıyorsa interceptor’da ham `SecurityContextHolder` erişimi thread değişimlerinde sorun çıkarabilir. Çağrı sınırında immutable bir context nesnesi oluşturup request attribute’a koyun; reactive akışlarda ise Reactor Context kullanın. MVC uygulamasında interceptor örneği şöyledir:
@Bean
RequestInterceptor authContextInterceptor() {
return template -> {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof JwtAuthenticationToken jwt) {
template.header("X-Tenant-Id", jwt.getToken().getClaimAsString("tenant"));
template.header("X-Subject-Id", jwt.getName());
}
};
}Bu kod yalnızca güvenilir servis ağı için başlangıç noktasıdır; `X-Tenant-Id` kullanıcıdan gelen header ile birleştirilmemelidir. Gateway’de istemci tarafından gönderilen aynı isimli header’ı silin, sonra doğrulanmış JWT claim’inden yeniden üretin. Ayrıca OPA input’una `caller_service` ekleyin: aynı kullanıcı yetkisi, örneğin raporlama servisi için geçerli iken ödeme servisi üzerinden uygulanmamalıdır. Bu, confused-deputy saldırısında downstream servisin kendi daha geniş teknik yetkisini kullanıcı yetkisi sanmasını önler.
Spring Framework eğitimi için test ve hata davranışı sözleşmesi
Bir spring framework eğitimi içinde yetkilendirme testini sadece 200 ve 403 kontrolüne indirgemeyin. OPA unavailable olduğunda read endpoint’lerin fail-closed, sağlık kontrolü gibi sınırlı altyapı endpoint’lerinin ise ayrı bir güvenlik zincirinde olması gerekir. OPA bağlantı hatasını `AccessDeniedException` olarak maskelemek operasyon ekibinin 403 artışını yanlış yorumlamasına neden olur; karar verilemedi durumunu ayrı metrikle sayın ve API sözleşmenizde kontrollü 503 üretin.
@Test
void foreignTenantIsDenied() {
opaStub.stubFor(post(urlEqualTo("/v1/data/authz/orders/allow"))
.willReturn(okJson("{\"result\":false}")));
webTestClient.get().uri("/orders/42")
.headers(h -> h.setBearerAuth(tokenFor("u-7", "tenant-b")))
.exchange()
.expectStatus().isForbidden();
}
@Test
void opaTimeoutIsReportedAsUnavailable() {
opaStub.stubFor(post(anyUrl()).willReturn(aResponse()
.withFixedDelay(1_500).withStatus(200)));
webTestClient.get().uri("/orders/42").exchange()
.expectStatus().isEqualTo(503);
}WireMock ile bu testleri çalıştırırken HTTP istemcisine gerçek timeout koyun; aksi halde `withFixedDelay` testi yalnızca yavaşlar, timeout davranışını doğrulamaz. Örneğin `RestClient` altyapısında connect timeout 200 ms, response timeout 500 ms tanımlayın ve bu sınırları OPA p99 ölçümünüz ile uyumlu seçin. `spring security` kararının 503’e dönmesi için `AuthorizationDeniedException` ile ağ hatasını aynı exception türünde toplamayın; özel `AuthorizationServiceUnavailableException` ve `@ControllerAdvice` ile Problem Details üretin.
Bu tasarım, java spring eğitimi veya spring boot kursu örneklerinde sık görülen `hasRole('ADMIN')` yaklaşımından daha fazla operasyonel disiplin ister: politika kodu test edilir, karar gecikmesi ayrı ölçülür, kaynak bağlamının sahibi sunucu olur ve policy dağıtımının etkisi revizyonla izlenir. Buna karşılık basit, tek uygulamalı ve nadiren değişen iki-üç rol için OPA eklemek gereksiz ağ bağımlılığıdır; aynı kuralları Spring Security authorization DSL içinde tutmak daha az hata yüzeyi yaratır.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Java Spring Boot ReactJS FullStack Eğitimi (Yıldız Teknik Üniversitesi SEM)
Sık Sorulan Sorular
Spring Security ile OPA kullanırken karar önbelleği güvenli mi?
Yalnızca kısa TTL’li, salt-okunur kararları cache’leyin. Anahtara subject, tenant, action, resource ve policy revision ekleyin. Rol, üyelik veya kaynak sahipliği değiştiğinde ilgili anahtarları invalidate edin; yalnızca kullanıcı kimliğiyle cache anahtarı kurmak farklı kaynakların kararını karıştırır.
Spring MVC uygulamasında OPA yetkilendirmesi filter mı @PreAuthorize mı olmalı?
HTTP route ve method bazlı kararlar için `authorizeHttpRequests(...).access(AuthorizationManager)` kullanın; karar controller’dan önce verilir. İş operasyonu seviyesindeki kontroller için `@PreAuthorize` ekleyin. Aynı bean içinden yapılan çağrılarda method-security proxy’si çalışmayacağından, self-invocation’a güvenmeyin.
Spring Cloud servisleri arasında kullanıcı JWT’si taşınmalı mı?
Varsayılan olarak hayır. Teknik servis çağrılarında client-credentials ve dar audience kullanın. Kullanıcı adına işlem gerekiyorsa token exchange tercih edin. Tenant veya subject header’ları ancak gateway’in istemci kaynaklı değerleri silip doğrulanmış claim’lerden yeniden üretmesi ve servis ağının mTLS ile korunması halinde anlamlıdır.
Spring REST API için OPA gecikmesi nasıl ölçülür?
OPA HTTP çağrısını Micrometer `Timer` ile ayrı ölçün ve Prometheus histogramını yayınlayın. Aynı k6 senaryosunu cache öncesi ve sonrası, aynı trafik oranı ve pod sayısıyla çalıştırın; p95/p99 karar süresi yanında 403 oranını, timeout sayısını ve policy revision dağılımını karşılaştırın.
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.


