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] ← ControllerHer 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/usersisteği geldiğinde →apiFilterChaineşleşir → HTTP Basic authentication/dashboardisteği geldiğinde → ilk zincir eşleşmez →webFilterChaindevreye 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 birSecurityFilterChain, tüm istekleri eşleştirir (catch-all). Bu yüzden@Orderdeğ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ıra | Filtre | Görev |
|---|---|---|
| 1 | DisableEncodeUrlFilter | URL'ye session ID eklenmesini engeller |
| 2 | WebAsyncManagerIntegrationFilter | Async isteklerde SecurityContext yönetimi |
| 3 | SecurityContextHolderFilter | SecurityContext'i yükler/temizler |
| 4 | HeaderWriterFilter | Güvenlik header'larını ekler |
| 5 | CsrfFilter | CSRF token doğrulaması |
| 6 | LogoutFilter | Logout isteğini işler |
| 7 | UsernamePasswordAuthenticationFilter | Form login credentials kontrolü |
| 8 | BasicAuthenticationFilter | HTTP Basic authentication |
| 9 | RequestCacheAwareFilter | Login sonrası orijinal isteğe yönlendirme |
| 10 | SecurityContextHolderAwareRequestFilter | Servlet API güvenlik metotları |
| 11 | AnonymousAuthenticationFilter | Anonim kullanıcı atama |
| 12 | ExceptionTranslationFilter | Güvenlik exception'larını HTTP yanıtına çevirme |
| 13 | AuthorizationFilter | Yetkilendirme 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 ekleraddFilterAfter(filter, referenceFilter)— Referans filtrenin sonrasına ekleraddFilterAt(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-3Bu, 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ınAuthentication 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:
Doğrulama öncesi (unauthenticated):
credentialsdolu (şifre),isAuthenticated = falseDoğrulama sonrası (authenticated):
principaldolu (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=TRACE2. 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/Afterile 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.
AI Asistan
Sorularını yanıtlamaya hazır