Spring Security 6 ile Stateless REST API Güvenliği

# Spring Security ile Güvenli API Mimarisi Kurulumu Spring Security, Spring tabanlı uygulamalarda kimlik doğrulama ve yetkilendirmeyi yöneten temel b...

Spring Security 6 ile Stateless REST API Güvenliği

Spring Security ile Güvenli API Mimarisi Kurulumu

Spring Security, Spring tabanlı uygulamalarda kimlik doğrulama ve yetkilendirmeyi yöneten temel bileşendir. Ancak varsayılan yapılandırması tarayıcı tabanlı oturum akışlarına göre tasarlandığı için, bir REST API'de doğru mimariyi kurmak ayrı bir dikkat gerektirir. Bu rehber, durumsuz (stateless) bir API için güvenlik zincirini adım adım kurar.

Önkoşullar ve Kapsam

  • Java 17 veya üzeri
  • Spring Boot 3.x (Spring Security 6.x ile birlikte gelir)
  • Maven veya Gradle
  • Temel HTTP, JWT ve Spring konfigürasyonu bilgisi

Spring Security 6, yapılandırma modelini önemli ölçüde değiştirdi: WebSecurityConfigurerAdapter kaldırıldı, lambda tabanlı DSL zorunlu hale geldi. Bu rehber Spring Security 6 ve sonrasını hedefler. Spring Security 5 kullanan bir projede WebSecurityConfigurerAdapter tabanlı eski örneklerle karşılaşırsanız, bunlar bu sürümde derlenmez.

1. Bağımlılıkları Ekleme

Maven kullanıyorsanız:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

spring-boot-starter-security eklendiği anda Spring Boot, tüm endpoint'leri varsayılan olarak korur ve başlangıçta konsola rastgele bir şifre yazar. Bu, geliştirme sırasında kafa karıştırıcı olabilir; asıl yapılandırmayı yazana kadar geçici bir davranış olduğunu bilin.

2. Güvenlik Filtre Zincirini Tanımlama

Spring Security 6'da SecurityFilterChain bean'i tanımlamak standart yaklaşımdır:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            );
        return http.build();
    }
}

Burada dikkat edilmesi gereken üç nokta var:

  • CSRF devre dışı bırakma: Tarayıcı tabanlı oturum çerezleri kullanılmadığı için CSRF korumasına gerek yoktur. Ancak API'niz çerez tabanlı kimlik doğrulama kullanıyorsa bu satırı kaldırmayın.
  • Stateless oturum: Sunucu tarafında oturum tutulmaz; her istek kendi kimlik bilgisini taşır. Yatay ölçeklemede oturum paylaşımı sorunu ortadan kalkar.
  • requestMatchers sırası: Kurallar yukarıdan aşağıya değerlendirilir. İlk eşleşen kural uygulanır, bu yüzden özel kuralları genel olanlardan önce yazın.

3. Şifre Kodlayıcı

Şifreleri asla düz metin saklamayın. PasswordEncoder bean'i tanımlayın:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

BCrypt, her şifre için farklı bir tuz üretir ve kasıtlı olarak yavaştır; kaba kuvvet saldırılarını maliyetli hale getirir. NoOpPasswordEncoder yalnızca test amaçlıdır ve üretimde kullanılmamalıdır. Spring Security, NoOpPasswordEncoder kullanımında uyarı üretir.

4. JWT Tabanlı Kimlik Doğrulama Katmanı

Stateless API'lerde yaygın yaklaşım, kullanıcıyı bir kez doğrulayıp imzalı bir token döndürmektir. Token'ı doğrulayan bir filtre, UsernamePasswordAuthenticationFilter'dan önce zincire eklenir:

public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtService jwtService;

    public JwtAuthFilter(JwtService jwtService) {
        this.jwtService = jwtService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            if (jwtService.isValid(token)) {
                Authentication auth = jwtService.toAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        filterChain.doFilter(request, response);
    }
}

Filtreyi zincire bağlarken konumunu doğru seçin:

http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

JwtService sınıfının içeriği kütüphane seçiminize bağlıdır. İmza doğrulamasında algoritmayı açıkça belirtin; token başlığındaki alg alanına körü körüne güvenmek, algoritma karışıklığı saldırılarına kapı aralar.

5. Yetkilendirme Kuralları ve Rol Modeli

hasRole("ADMIN") çağrısı, arka planda ROLE_ADMIN yetkisini arar. Yetkileri ROLE_ öneki olmadan atarsanız kural sessizce eşleşmez ve istek reddedilir. Bu, en sık yapılan yapılandırma hatalarından biridir.

Metot seviyesinde ek kontrol için @EnableMethodSecurity ekleyip servis katmanında kullanabilirsiniz:

@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public UserDto getUser(Long userId) { ... }

URL bazlı kurallar kaba bir kapıdır; nesne sahipliği gibi ince kontrolleri servis katmanına bırakın.

Yaygın Hatalar

  • CSRF'i her API'de kapatmak: Çerez tabanlı kimlik doğrulama kullanıyorsanız CSRF korumasını açık bırakın.
  • permitAll() kurallarını en üste koymak: Geniş kurallar önce yazılırsa, sonraki özel kurallar hiç değerlendirilmez.
  • Token süresini uzun tutmak: Erişim token'ları kısa ömürlü, yenileme token'ları ayrı ve iptal edilebilir olmalı.
  • Hata mesajlarında ayrım yapmak: "Kullanıcı yok" ile "şifre yanlış" ayrımı, kullanıcı numaralandırmasına (enumeration) izin verir. Her iki durumda aynı genel mesajı döndürün.
  • Yetki kontrolünü yalnızca ön yüze bırakmak: İstemci tarafındaki gizleme bir güvenlik önlemi değildir; her istek sunucuda yeniden doğrulanmalıdır.

Güvenlik Etkileri

Kimlik doğrulama katmanı yanlış kurulduğunda etki yalnızca "yetkisiz erişim" ile sınırlı kalmaz. Yanlış yapılandırılmış bir filtre zinciri, kimlik doğrulamayı tamamen atlayabilir; örneğin anyRequest().permitAll() satırı fark edilmeden kalırsa tüm endpoint'ler açık hale gelir. Yapılandırmanızı testlerle doğrulayın: korunması gereken bir endpoint'e token olmadan istek atıp 401 veya 403 aldığınızı kontrol eden entegrasyon testleri yazın.

Sonuç

Güvenli bir API mimarisi; stateless oturum politikası, doğru sıralanmış yetkilendirme kuralları, güçlü şifre kodlama ve imza doğrulaması yapılan token katmanından oluşur. Yapılandırmanın doğruluğunu varsayımla değil, testlerle doğrulayın. Sürüm yükseltmelerinde Spring Security'nin DSL değişikliklerini takip etmek, eski örnekleri körü körüne kopyalamaktan sizi korur.

İLGİLİ İÇERİKLER

Bunları da Okumak İsteyebilirsiniz