in

Spring Security Keycloak Entegrasyonu: OAuth2 ve OIDC Rehberi

Her kurumsal uygulamada girişi yeniden yazmak yerine merkezi kimlik sağlayıcı: Keycloak. OAuth2/OIDC akışlarının hangisinin nerede kullanılacağını, realm ve client kurulumunu, Spring Boot resource server'da JWT doğrulama ve rol eşlemeyi, servisler arası client credentials'ı, tarayıcı uygulamaları için BFF desenini ve üretim kontrol listesini kod örnekleriyle anlatıyoruz.

React istemci, Keycloak kimlik sağlayıcı ve Spring Boot API arasındaki OAuth2 Authorization Code akışı

Kurumsal ekiplerin çoğu her yeni uygulamada aynı şeyi yeniden yazar: kullanıcı tablosu, şifre hash'i, giriş ekranı, "şifremi unuttum", oturum yönetimi. Beşinci uygulamada beş farklı kullanıcı deposu, beş farklı şifre politikası ve tek bir MFA bile olmayan bir manzara oluşur. Çözüm, kimliği uygulamadan çıkarıp merkezi bir kimlik sağlayıcıya (IdP) taşımaktır; açık kaynak dünyasında bunun standardı Keycloak, protokolü OAuth 2.0 ve OpenID Connect'tir.

Bu yazıda OAuth2/OIDC akışlarının hangisinin nerede kullanılacağını, Keycloak'ta realm ve client kurulumunu, Spring Boot'u resource server olarak yapılandırıp JWT doğrulama ve rol eşlemeyi, servisler arası client credentials akışını, tarayıcı uygulamaları için BFF desenini, yetkilendirme modelini, üretim kontrol listesini ve sahada gördüğümüz hataları anlatıyoruz.

Kavramlar ve akışlar

OAuth 2.0 dört rol tanımlar: kaynak sahibi (kullanıcı), istemci (React uygulaması, mobil uygulama, başka bir servis), yetkilendirme sunucusu (Keycloak) ve kaynak sunucusu (Spring Boot API). OpenID Connect, OAuth2'nin üzerine kimlik katmanı ekler: "kim giriş yaptı" bilgisini taşıyan ID token. Uygulama üç token görür: kısa ömürlü access token (API'ye gider, JWT), ID token (istemciye kullanıcı bilgisi) ve refresh token (yenisini almak için).

Akış

Kim kullanır

Not

Authorization Code + PKCE

Tarayıcı uygulamaları (SPA), mobil, sunucu taraflı web

Standart kullanıcı girişi; PKCE her istemcide açık

Client Credentials

Servisten servise (kullanıcı yok): toplu iş, entegrasyon

Client id + secret ile; kullanıcı bağlamı taşımaz

Device Authorization

Klavyesiz cihaz, CLI araçları

Nadir; kod göster–tarayıcıdan onayla

Implicit, Resource Owner Password

—

Kullanımdan kalktı; yeni sistemde yasak (şifre uygulamaya girmez)

Mimari: kim neyi taşır?

[React SPA] ──(1) giriş yönlendirmesi──► [Keycloak]  (kullanıcı adı/şifre, MFA, kurumsal AD ile federasyon)
[React SPA] ◄──(2) authorization code──  [Keycloak]
[React SPA] ──(3) code + PKCE verifier──► [Keycloak] ──► access + ID + refresh token
[React SPA] ──(4) Authorization: Bearer <access>──► [Spring Boot API] ──(5) JWKS ile imza doğrulama──► [Keycloak]

Tarayıcıda token saklamak (localStorage) XSS'e karşı savunmasızdır. Kurumsal tarayıcı uygulamalarında önerilen düzen BFF (Backend for Frontend)'dir: tarayıcı yalnızca HttpOnly, Secure, SameSite çerezle BFF'ye konuşur; token'lar BFF'nin sunucu tarafındaki oturumunda yaşar; BFF API çağrılarına Bearer ekler. Spring Security'nin OAuth2 client desteği bunu hazır sunar; Spring Cloud Gateway'in TokenRelay filtresi aynı işi geçit katmanında yapar.

Keycloak kurulumu: realm ve client

  • Realm: Kullanıcı, rol ve client'ların yaşadığı izole alan; şirket başına bir realm (örn. acme). Issuer URL: https://kimlik.acme.com/realms/acme.

  • Client'lar: SPA için public client (secret yok, PKCE zorunlu, yönlendirme URI'ları kesin, web origin'leri kısıtlı); BFF ve servisler için confidential client (secret ya da sertifika); API için ayrı bir client (audience olarak kullanılır, token üretmez).

  • Roller ve gruplar: Realm rolleri (muhasebe, yonetici) ya da client rolleri; kullanıcılar gruplara, gruplar rollere. Token'a hangi claim'lerin gireceği mapper ile belirlenir: roller, e-posta, kiracı özniteliği (tenant_id).

  • Token ömürleri: Access 5 dk, refresh 30 dk (döndürmeli), SSO oturumu iş politikasına göre (8 saat); "uzun yaşayan access token" istekleri reddedilir.

  • Kurumsal entegrasyon: LDAP/Active Directory kullanıcı federasyonu, Entra ID/Google ile kimlik aracılığı (identity brokering), OTP/WebAuthn ile MFA, brute-force koruması, parola politikası, kurumsal tema.

Spring Boot resource server: JWT doğrulama ve rol eşleme

# application.yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://kimlik.acme.com/realms/acme     # OIDC discovery ile JWKS otomatik bulunur
@Configuration
@EnableMethodSecurity
public class GuvenlikAyari {

    @Bean
    SecurityFilterChain api(HttpSecurity http, JwtAuthenticationConverter converter) throws Exception {
        return http
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(AbstractHttpConfigurer::disable)                      // Bearer ile durumsuz API; çerez yoksa CSRF gerekmez
            .authorizeHttpRequests(a -> a
                .requestMatchers("/actuator/health/**").permitAll()
                .requestMatchers("/api/**").authenticated()
                .anyRequest().denyAll())
            .oauth2ResourceServer(o -> o.jwt(j -> j.jwtAuthenticationConverter(converter)))
            .build();
    }

    /** Keycloak'ın realm_access.roles claim'ini Spring'in ROLE_ yetkilerine çevirir */
    @Bean
    JwtAuthenticationConverter keycloakRolDonusturucu() {
        var converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt -> {
            Map<String, Object> realm = jwt.getClaimAsMap("realm_access");
            List<String> roller = realm == null ? List.of() : (List<String>) realm.getOrDefault("roles", List.of());
            return roller.stream().map(r -> new SimpleGrantedAuthority("ROLE_" + r.toUpperCase())).collect(Collectors.toList());
        });
        converter.setPrincipalClaimName("preferred_username");
        return converter;
    }

    /** issuer + audience doğrulama: yalnızca bu API için üretilmiş token'lar kabul edilir */
    @Bean
    JwtDecoder jwtDecoder(OAuth2ResourceServerProperties props) {
        String issuer = props.getJwt().getIssuerUri();
        NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation(issuer);
        OAuth2TokenValidator<Jwt> audience = new JwtClaimValidator<List<String>>("aud", aud -> aud != null && aud.contains("siparis-api"));
        decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(JwtValidators.createDefaultWithIssuer(issuer), audience));
        return decoder;
    }
}

@RestController
@RequestMapping("/api/faturalar")
class FaturaController {
    @PreAuthorize("hasRole('MUHASEBE')")
    @PostMapping
    FaturaDto olustur(@RequestBody @Valid FaturaIstegi istek, @AuthenticationPrincipal Jwt jwt) {
        String kiraci = jwt.getClaimAsString("tenant_id");             // kiracı bağlamı doğrulanmış token'dan
        return servis.olustur(kiraci, jwt.getSubject(), istek);
    }
}

Üç doğrulama ayrılmaz: imza (JWKS ile), issuer (token gerçekten bizim realm'den mi) ve audience (bu API için mi üretilmiş; aksi hâlde başka bir uygulama için alınan token bu API'de geçer). Keycloak'ta audience claim'i bir mapper ile eklenir. Saat sapması için 30–60 saniyelik tolerans varsayılandır; JWKS anahtarları önbelleklenir ve anahtar döndürmede otomatik yenilenir.

Servisler arası: client credentials

spring:
  security:
    oauth2:
      client:
        registration:
          stok-servisi:
            provider: keycloak
            client-id: siparis-servisi
            client-secret: ${STOK_CLIENT_SECRET}
            authorization-grant-type: client_credentials
            scope: stok:oku
        provider:
          keycloak:
            issuer-uri: https://kimlik.acme.com/realms/acme
@Bean
RestClient stokClient(RestClient.Builder builder, OAuth2AuthorizedClientManager manager) {
    var interceptor = new OAuth2ClientHttpRequestInterceptor(manager);   // Spring Security 6.4+: token alma, önbellekleme, yenileme otomatik
    interceptor.setClientRegistrationIdResolver(req -> "stok-servisi");
    return builder.baseUrl("https://stok.acme.internal").requestInterceptor(interceptor).build();
}

Kullanıcı bağlamının servisler arasında taşınması gerekiyorsa (denetim, kiracı) gelen access token ileriye aktarılır (token relay) ya da token exchange kullanılır; servisin kendi kimliğiyle kullanıcı adına iş yapması izlenebilirliği bozar.

Yetkilendirme modeli: token'da kaba, uygulamada ince

Token'da yalnızca kaba roller ve kiracı taşınır (muhasebe, tenant_id); "bu kullanıcı bu faturayı görebilir mi" gibi ince kararlar uygulamada verilir: sahiplik, kiracı eşleşmesi, kayıt durumu. Elli izin claim'i taşıyan token şişer, her istekle ağda dolaşır ve izin değişince ancak token yenilenince yansır. İnce yetki için uygulama içi politika servisi (ya da OPA gibi bir motor) ve @PreAuthorize ifadeleri; roller koda gömülmez, yapılandırmadan gelir.

Üretim kontrol listesi

  • Her yerde HTTPS; Keycloak ters vekil arkasında ve X-Forwarded-* doğru; yönlendirme URI'ları joker karaktersiz.

  • Access token ≤ 5–10 dk, refresh token döndürmeli ve yeniden kullanım tespiti açık; oturum sonlandırma için back-channel logout.

  • Keycloak yüksek erişilebilir: en az iki örnek, paylaşımlı PostgreSQL, önbellek kümesi; realm dışa aktarımı yedekte; sürüm yükseltmeleri test realm'inde prova.

  • Denetim olayları (giriş, başarısız giriş, yönetici işlemleri) merkezi loga; olağandışı giriş için uyarı.

  • Client secret'lar gizli yönetiminde ve döndürülebilir; yönetici hesabı için MFA zorunlu; uygulama Keycloak Admin API'sini yalnızca dar yetkili servis hesabıyla kullanır.

  • CORS yalnızca bilinen origin'ler; hız sınırı token uç noktasında; /actuator ve yönetim uçları dışarıya kapalı.

Sahada sık yapılan hatalar

  1. Token'ı localStorage'a koymak: Tek XSS ile oturum çalınır; BFF ya da en azından bellekte + kısa ömür.

  2. Yalnızca imzayı doğrulamak: Issuer ve audience kontrolü olmadan başka realm'in ya da başka uygulamanın token'ı geçer.

  3. Uzun ömürlü access token: "Kullanıcı sık giriş yapmasın" isteği refresh token ve SSO oturumuyla karşılanır, access token uzatılarak değil.

  4. Şifreyi uygulamaya almak (ROPC): MFA ve federasyon devre dışı kalır; giriş her zaman Keycloak'ın ekranında.

  5. Rolleri koda gömmek: if (kullanici.rol == "yonetici") yerine yetki ifadeleri ve yapılandırma.

  6. Keycloak'ı tek örnek çalıştırmak: IdP düştüğünde tüm uygulamalar düşer; HA şart.

Sık sorulan sorular

Keycloak mı, Entra ID / Okta / Auth0 mı?

Hepsi aynı protokolleri konuşur; Spring tarafı değişmez. Keycloak açık kaynak, kendi altyapınızda, lisans maliyeti yok ama işletme sorumluluğu sizde. Yönetilen servisler işletmeyi alır, kullanıcı başına ücretlendirir. Şirket zaten Microsoft 365 kullanıyorsa Entra ID'yi kimlik kaynağı, Keycloak'ı uygulama katmanı IdP'si olarak birleştirmek yaygın bir düzendir.

Oturum (session) tabanlı güvenlikten JWT'ye geçmeli miyiz?

Tek bir sunucu taraflı web uygulaması için Spring'in OAuth2 login + oturum modeli yeterli ve daha basittir; JWT'nin değeri API'yi birden çok istemcinin ve servisin kullandığı yerdedir. İkisi birlikte de olur: BFF oturumu tutar, API JWT doğrular.

Legacy uygulamayı Keycloak'a nasıl bağlarız?

Kodu değiştiremiyorsanız önüne bir OIDC vekili (oauth2-proxy gibi) koyup kimliği başlıkla iletmek hızlı çözümdür; kod değiştirilebiliyorsa Spring Security'nin OAuth2 login desteği ile birkaç günlük iş.

Sonuç

Kurumsal kimlik doğrulamanın doğru düzeni: giriş Keycloak'ta (MFA, federasyon, parola politikası tek yerde), tarayıcı uygulamaları BFF ile çerez üzerinden, API'ler imza + issuer + audience doğrulayan durumsuz resource server, servisler arası client credentials, token'da kaba roller, uygulamada ince yetki ve yüksek erişilebilir bir IdP. Bu düzen bir kez kurulur; sonraki her uygulama giriş ekranı yazmadan doğar.

Uygulamalarınızı merkezi kimlik doğrulamaya taşımak ya da mevcut Keycloak kurulumunuzu üretim standardına getirmek için kurumsal uygulama geliştirme ile web ve e-ticaret geliştirme hizmetlerimize göz atabilirsiniz.