← Kursa Dön
📄 Text · 30 min

Security Filter Chain Mimarisi

Spring Security'nin kalbi, Servlet Filter mimarisi üzerine kuruludur. Her HTTP isteği, Controller'a ulaşmadan önce bir dizi güvenlik filtresinden geçer. Bu filtreler zinciri (filter chain), authentication, authorization, CSRF kontrolü, session yönetimi ve daha fazlasını gerçekleştirir. Bu mimariyi anlamak, Spring Security'yi etkili bir şekilde özelleştirmenin ön koşuludur.

Bir havaalanının güvenlik kontrolünü düşünün. Yolcular sırayla birkaç kontrol noktasından geçer: pasaport kontrolü, bagaj tarama, metal dedektör, kamera kayıt. Her kontrol noktası bağımsız bir iş yapar ama hepsi birlikte güvenliği sağlar. Spring Security'nin filter chain'i tam da böyle çalışır — her filtre belirli bir güvenlik görevini yerine getirir.

Servlet Filter Temelleri

Java Servlet spesifikasyonunda Filter, HTTP isteğini ve yanıtını Controller'a ulaşmadan önce (veya sonra) işleyebilen bir bileşendir:

// Standart Servlet Filter
public class LoggingFilter implements jakarta.servlet.Filter {

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        long startTime = System.currentTimeMillis();
        
        // PRE-PROCESSING — İstek Controller'a ulaşmadan önce
        System.out.println("İstek geldi: " + httpRequest.getMethod() 
            + " " + httpRequest.getRequestURI());
        
        chain.doFilter(request, response); // Zincirdeki sonraki filtreye geç
        
        // POST-PROCESSING — Controller işini bitirdikten sonra
        long duration = System.currentTimeMillis() - startTime;
        System.out.println("Yanıt gönderiliyor (" + duration + "ms)");
    }
}

chain.doFilter() çağrısı kritiktir — bir sonraki filtreye geçişi sağlar. Bu çağrı yapılmazsa istek zincirde ilerlemez ve Controller'a asla ulaşmaz. Spring Security tam da bu mekanizmayı kullanır — authentication başarısız olursa chain.doFilter() çağrılmaz ve istek reddedilir.

Filter'ların çalışma sırası bir zincir gibidir:

İstek → [Filter A] → [Filter B] → [Filter C] → Controller
                                                      ↓
Yanıt ← [Filter A] ← [Filter B] ← [Filter C] ← Controller

Her filter hem isteği (pre-processing) hem yanıtı (post-processing) işleyebilir. Bu, AOP (Aspect-Oriented Programming) benzeri bir kesme noktası (crosscutting concern) mekanizmasıdır.

DelegatingFilterProxy: Köprü

Servlet container (Tomcat) Spring Bean'lerini bilmez; Spring de Servlet Filter'ları doğrudan yönetemez. DelegatingFilterProxy, bu iki dünya arasında köprü görevi görür.

HTTP İsteği
    ↓
[Servlet Container (Tomcat)]
    ↓
[DelegatingFilterProxy]     ← Servlet Filter (container bunu bilir)
    ↓
[FilterChainProxy]          ← Spring Bean (Spring bunu yönetir)
    ↓
[SecurityFilterChain #1]    → URL eşleşme varsa bu zincir çalışır
[SecurityFilterChain #2]    → Eşleşme yoksa sıradaki denenir
    ↓
[DispatcherServlet → Controller]

DelegatingFilterProxy, Servlet container'da springSecurityFilterChain adıyla kayıtlı bir filter olarak çalışır ve gelen her isteği Spring'in yönettiği FilterChainProxy'ye yönlendirir.

Spring Boot bu kurulumu otomatik yapar — SecurityAutoConfiguration sayesinde DelegatingFilterProxy ve FilterChainProxy otomatik oluşturulur. Elle yapılandırma gerekmez.

FilterChainProxy ve SecurityFilterChain

FilterChainProxy, birden fazla SecurityFilterChain yönetir. Her SecurityFilterChain, belirli URL pattern'lerine eşleşen bir filtre dizisidir. İlk eşleşen zincir çalışır, diğerleri atlanır:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // API endpoint'leri için ayrı güvenlik zinciri
    @Bean
    @Order(1) // Öncelik sırası — düşük sayı daha yüksek öncelik
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        return http
            .securityMatcher("/api/**") // Bu zincir sadece /api/** istekleri için
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .httpBasic(Customizer.withDefaults()) // API: HTTP Basic
            .csrf(csrf -> csrf.disable())         // API: CSRF kapalı
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .build();
    }

    // Web sayfaları için ayrı güvenlik zinciri
    @Bean
    @Order(2) // API zinciri eşleşmezse bu zincir denenir
    public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/register", "/css/**").permitAll()
                .anyRequest().authenticated())
            .formLogin(form -> form  // Web: Form login
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard"))
            .build();
    }
}

@Order anotasyonu zincirlerin öncelik sırasını belirler:

  • /api/users isteği geldiğinde → apiFilterChain eşleşir → HTTP Basic authentication

  • /dashboard isteği geldiğinde → ilk zincir eşleşmez → webFilterChain devreye girer → form login

Bu yapı, aynı uygulama içinde farklı güvenlik politikalarını uygulamayı sağlar. API istemcileri token/basic auth kullanırken, web kullanıcıları form login ile giriş yapar.

💡 İpucu: securityMatcher() tanımlamayan bir SecurityFilterChain, tüm istekleri eşleştirir (catch-all). Bu yüzden @Order değeri en yüksek olmalıdır — aksi takdirde diğer zincirlere sıra gelmez.

Varsayılan Güvenlik Filtreleri

Spring Security, bir SecurityFilterChain içinde yaklaşık 15-16 filtre çalıştırır. Bunların sırası sabittir ve her birinin belirli bir görevi vardır:

SıraFiltreGörev
1DisableEncodeUrlFilterURL'ye session ID eklenmesini engeller
2WebAsyncManagerIntegrationFilterAsync isteklerde SecurityContext yönetimi
3SecurityContextHolderFilterSecurityContext'i yükler/temizler
4HeaderWriterFilterGüvenlik header'larını ekler
5CsrfFilterCSRF token doğrulaması
6LogoutFilterLogout isteğini işler
7UsernamePasswordAuthenticationFilterForm login credentials kontrolü
8BasicAuthenticationFilterHTTP Basic authentication
9RequestCacheAwareFilterLogin sonrası orijinal isteğe yönlendirme
10SecurityContextHolderAwareRequestFilterServlet API güvenlik metotları
11AnonymousAuthenticationFilterAnonim kullanıcı atama
12ExceptionTranslationFilterGüvenlik exception'larını HTTP yanıtına çevirme
13AuthorizationFilterYetkilendirme kontrolü

Bu filtrelerin sırasını bilmek, özellikle custom filtre eklediğinizde hayati önem taşır. Kendi filtrenizi yanlış sıraya koyarsanız, güvenlik açıkları oluşabilir.

Custom Filter Ekleme

Kendi güvenlik filtrenizi zincire ekleyebilirsiniz:

// Custom JWT Authentication Filter
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtService jwtService, 
                                    UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain filterChain)
            throws ServletException, IOException {
        
        String authHeader = request.getHeader("Authorization");
        
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response); // Token yoksa devam et
            return;
        }

        String jwt = authHeader.substring(7);
        String username = jwtService.extractUsername(jwt);

        if (username != null && SecurityContextHolder.getContext()
                .getAuthentication() == null) {
            
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(
                    new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

// Filter'ı zincire ekleme
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .addFilterBefore(
            new JwtAuthenticationFilter(jwtService, userDetailsService),
            UsernamePasswordAuthenticationFilter.class) // Bu filtreden ÖNCE ekle
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .build();
}

Filter ekleme metotları:

  • addFilterBefore(filter, referenceFilter) — Referans filtrenin öncesine ekler

  • addFilterAfter(filter, referenceFilter) — Referans filtrenin sonrasına ekler

  • addFilterAt(filter, referenceFilter) — Referans filtrenin yerine ekler (referans filtre kaldırılmaz, aynı sırada çalışır)

⚠️ Dikkat: JWT filter'ı UsernamePasswordAuthenticationFilter'dan önce eklenir çünkü JWT token'ı form login'den önce kontrol edilmelidir. Sıralama yanlış olursa authentication çalışmaz.

SecurityContext ve SecurityContextHolder

SecurityContext, o anki isteğin güvenlik bilgilerini (authentication nesnesi) tutan konteynerdir. SecurityContextHolder, bu context'e erişim sağlayan statik yardımcı sınıftır:

// Herhangi bir yerden mevcut kullanıcıya erişim
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();

String username = auth.getName();                // Kullanıcı adı
Object principal = auth.getPrincipal();          // UserDetails nesnesi
Collection<? extends GrantedAuthority> authorities = 
    auth.getAuthorities();                       // Roller/yetkiler
boolean isAuthenticated = auth.isAuthenticated(); // Giriş yapmış mı?

// Utility metodu
public static String getCurrentUsername() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth == null || !auth.isAuthenticated() 
            || auth instanceof AnonymousAuthenticationToken) {
        return null;
    }
    return auth.getName();
}

ThreadLocal Stratejisi

SecurityContextHolder varsayılan olarak ThreadLocal stratejisi kullanır — her thread kendi SecurityContext'ini taşır:

Thread-1 (Kullanıcı: Ali, Role: ADMIN)  → SecurityContext-1
Thread-2 (Kullanıcı: Ayşe, Role: USER)  → SecurityContext-2
Thread-3 (Anonim)                        → SecurityContext-3

Bu, aynı anda gelen farklı isteklerin birbirinin güvenlik bilgilerini görmemesini sağlar. Ancak asenkron işlemlerde (CompletableFuture, @Async) dikkatli olunmalıdır — yeni thread'lere SecurityContext otomatik aktarılmaz:

// ❌ SORUN — Yeni thread'de SecurityContext yok
@Async
public void sendEmailAsync(String to) {
    String user = SecurityContextHolder.getContext()
        .getAuthentication().getName(); // NullPointerException!
}

// ✅ ÇÖZÜM — SecurityContext'i aktarma stratejisi
SecurityContextHolder.setStrategyName(
    SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

// Veya DelegatingSecurityContextRunnable kullanın

Authentication Nesnesi

Authentication arayüzü, kimlik doğrulama bilgilerini taşır:

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities(); // Roller/yetkiler
    Object getCredentials();  // Kimlik bilgisi (genellikle şifre)
    Object getDetails();      // Ek detaylar (IP adresi, session ID)
    Object getPrincipal();    // Ana kimlik (genellikle UserDetails)
    boolean isAuthenticated(); // Kimlik doğrulandı mı?
}

Authentication nesnesi iki durumda olabilir:

  1. Doğrulama öncesi (unauthenticated): credentials dolu (şifre), isAuthenticated = false

  2. Doğrulama sonrası (authenticated): principal dolu (UserDetails), isAuthenticated = true, credentials temizlenmiş

Doğrulama sonrası credentials'ın temizlenmesi güvenlik açısından önemlidir — bellekte şifre tutulmaz. Bu, Spring Security'nin "fail securely" prensibinin uygulamasıdır.

Authentication Flow (Tam Akış)

Form login senaryosunda authentication akışı adım adım şöyledir:

1. Kullanıcı POST /login (username + password)
       ↓
2. UsernamePasswordAuthenticationFilter isteği yakalar
       ↓
3. UsernamePasswordAuthenticationToken oluşturulur (unauthenticated)
   [principal=username, credentials=password, authenticated=false]
       ↓
4. AuthenticationManager.authenticate() çağrılır
       ↓
5. AuthenticationManager → ProviderManager → AuthenticationProvider listesi
       ↓
6. DaoAuthenticationProvider (varsayılan) seçilir
       ↓
7. UserDetailsService.loadUserByUsername(username) çağrılır
       ↓
8. UserDetailsService → veritabanı/bellek/LDAP'den UserDetails yüklenir
       ↓
9. PasswordEncoder.matches(rawPassword, encodedPassword) ile şifre doğrulanır
       ↓
10. Başarılıysa → Authentication nesnesi (authenticated) oluşturulur
    [principal=UserDetails, credentials=null, authenticated=true]
       ↓
11. SecurityContextHolder'a set edilir
       ↓
12. Session'a kaydedilir (HttpSessionSecurityContextRepository)
       ↓
13. AuthenticationSuccessHandler çalışır (yönlendirme)

Bu akış karmaşık görünebilir, ancak her adımın bir amacı vardır ve her biri bağımsız olarak özelleştirilebilir. Sonraki derslerde bu bileşenlerin her birini ayrı ayrı yapılandıracağız.

AuthenticationManager ve ProviderManager

// AuthenticationManager — tek metotlu arayüz
public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) 
        throws AuthenticationException;
}

// ProviderManager — AuthenticationManager'ın varsayılan implementasyonu
// Birden fazla AuthenticationProvider'ı dener
public class ProviderManager implements AuthenticationManager {
    private List<AuthenticationProvider> providers;
    
    public Authentication authenticate(Authentication auth) {
        for (AuthenticationProvider provider : providers) {
            if (provider.supports(auth.getClass())) {
                return provider.authenticate(auth);
            }
        }
        throw new ProviderNotFoundException("No provider found");
    }
}

Bu yapı, farklı authentication mekanizmalarını (form login, LDAP, OAuth2) aynı anda desteklemenizi sağlar. Her mekanizma için ayrı bir AuthenticationProvider tanımlarsınız.

Debug İpuçları

Spring Security sorunlarını ayıklamak zor olabilir — "neden 403 alıyorum?" sorusu çok yaygındır. İşte sistematik debug yaklaşımı:

1. Loglama Seviyesini Artırın

# Filtre zincirini ve karar sürecini görmek için
logging.level.org.springframework.security=DEBUG

# Daha detaylı — her filtre çağrısını gösterir
logging.level.org.springframework.security.web.FilterChainProxy=TRACE

# Authorization kararlarını görmek için
logging.level.org.springframework.security.access=DEBUG
logging.level.org.springframework.security.authorization=TRACE

2. Filter Chain'i Listeleme

@Component
public class SecurityFilterLogger implements CommandLineRunner {

    @Autowired
    private FilterChainProxy filterChainProxy;

    @Override
    public void run(String... args) {
        filterChainProxy.getFilterChains().forEach(chain -> {
            System.out.println("=== Filter Chain ===");
            chain.getFilters().forEach(filter -> 
                System.out.println("  " + filter.getClass().getSimpleName()));
        });
    }
}

3. Authentication Events Dinleme

@Component
public class AuthEventLogger {

    @EventListener
    public void onSuccess(AuthenticationSuccessEvent event) {
        log.info("Login başarılı: {}", event.getAuthentication().getName());
    }

    @EventListener
    public void onFailure(AbstractAuthenticationFailureEvent event) {
        log.warn("Login başarısız: {} — Sebep: {}", 
            event.getAuthentication().getName(),
            event.getException().getMessage());
    }
}

💡 İpucu: Güvenlik sorunlarını çözerken ilk adım her zaman log seviyesini artırmak olmalıdır. Spring Security detaylı loglar üretir — hangi filtrelerin çalıştığını, hangi authentication provider'ın denendiğini ve neden erişimin reddedildiğini gösterir.

Özet

  • Spring Security, Servlet Filter mimarisi üzerine kuruludur. Her istek bir dizi güvenlik filtresinden geçer.

  • DelegatingFilterProxy, Servlet container ile Spring arasında köprü kurar. FilterChainProxy, birden fazla SecurityFilterChain'i yönetir.

  • SecurityFilterChain, belirli URL pattern'lerine eşleşen filtre dizisidir. Birden fazla zincir tanımlayarak API ve web için farklı güvenlik politikaları uygulanabilir.

  • SecurityContext ve SecurityContextHolder, o anki isteğin authentication bilgisini ThreadLocal stratejisiyle taşır.

  • Authentication akışı: Filter → AuthenticationManager → ProviderManager → AuthenticationProvider → UserDetailsService → PasswordEncoder → SecurityContext.

  • Custom filter eklerken addFilterBefore/After ile doğru sıraya yerleştirin. Yanlış sıralama güvenlik açıklarına yol açar.

  • Debug için loglama seviyesini artırın ve authentication event'lerini dinleyin.

  • Filter chain Spring Boot'un en güçlü ve en esnek güvenlik mekanizmasıdır. Doğru anlaşıldığında, her türlü authentication senaryosunu uygulayabilirsiniz.