SecurityFilterChain Yapılandırması
Spring Security'nin yapılandırmasında modern yaklaşım, SecurityFilterChain bean'i döndüren bir @Bean metodu tanımlamaktır. Eski WebSecurityConfigurerAdapter sınıfı Spring Security 5.7'de deprecated oldu ve 6.0'da kaldırıldı. Yeni yaklaşım hem daha esnek hem de daha okunabilirdir — Lambda DSL ile her güvenlik özelliği akıcı bir şekilde yapılandırılır.
Bir binanın güvenlik sistemi kurulumunu düşünün. Eski sistemde tek bir kontrol paneli vardı ve her şeyi oradan yönetirdiniz (WebSecurityConfigurerAdapter). Yeni sistemde ise her bölüm için bağımsız kontrol üniteleri var — giriş kapısı, asansör, otopark, yangın sistemi hepsi ayrı yapılandırılır ama birlikte çalışır. SecurityFilterChain bean yaklaşımı tam da bu modüler yapıyı sağlar.
Temel SecurityFilterChain Yapılandırması
@Configuration
@EnableWebSecurity // Web güvenliğini aktifleştirir
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/about").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.deleteCookies("JSESSIONID")
.permitAll()
)
.build();
}
}Bu yapılandırma şunları söyler:
Ana sayfa, home ve about herkese açık (anonim kullanıcılar dahil)
/admin/**yolları sadece ADMIN rolüne sahip kullanıcılara/api/**yolları USER veya ADMIN rolüne sahip kullanıcılaraGeri kalan her şey en azından giriş yapmış olmayı gerektirir
HttpSecurity DSL Detayları
HttpSecurity, Lambda DSL (Domain Specific Language) ile yapılandırılır. Her metot bir güvenlik özelliğini yapılandırır. Bu DSL, Spring Security 5.2'de tanıtıldı ve 6.0'da standart hale geldi.
authorizeHttpRequests — Endpoint Yetkilendirmesi
Bu en kritik yapılandırmadır — hangi URL'lerin kime açık olduğunu belirler:
http.authorizeHttpRequests(auth -> auth
// ─── Public Endpoint'ler ─────────────────────────
// Herkese açık (anonim dahil)
.requestMatchers("/public/**", "/about", "/contact").permitAll()
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers("/api/auth/login", "/api/auth/register").permitAll()
// ─── Authenticated (Giriş yapmış herkes) ─────────
.requestMatchers("/profile/**", "/settings/**").authenticated()
.requestMatchers("/api/users/me").authenticated()
// ─── Rol Tabanlı Yetkilendirme ──────────────────
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/editor/**").hasAnyRole("EDITOR", "ADMIN")
.requestMatchers("/moderator/**").hasRole("MODERATOR")
// ─── Authority (İzin) Tabanlı ────────────────────
.requestMatchers("/reports/**").hasAuthority("REPORT_VIEW")
.requestMatchers("/reports/export/**").hasAuthority("REPORT_EXPORT")
.requestMatchers(HttpMethod.DELETE, "/api/**").hasAuthority("DELETE_PRIVILEGE")
// ─── HTTP Metot Bazlı ────────────────────────────
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/products/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/products/**").hasRole("ADMIN")
// ─── Kalan Her Şey ──────────────────────────────
// En sona yazılmalı — yukarıdaki kurallarla eşleşmeyen tüm istekler
.anyRequest().authenticated()
);⚠️ Sıralama kuralı:
requestMatcherskuralları yukarıdan aşağıya değerlendirilir ve ilk eşleşen kural uygulanır. Bu nedenle daha spesifik kurallar önce, genel kurallar sonra yazılmalıdır.anyRequest()her zaman en sonda olmalıdır.
// ❌ YANLIŞ SIRALAMA — anyRequest() her şeyi yakalar, alttakiler çalışmaz
.anyRequest().authenticated()
.requestMatchers("/admin/**").hasRole("ADMIN") // Buraya asla ulaşılmaz!
// ✅ DOĞRU SIRALAMA — spesifik kurallar önce
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()hasRole() vs hasAuthority()
// hasRole("ADMIN") → ROLE_ADMIN authority'sini arar (ROLE_ prefix otomatik eklenir)
.requestMatchers("/admin/**").hasRole("ADMIN")
// hasAuthority("ROLE_ADMIN") → Açıkça ROLE_ADMIN arar
.requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
// İkisi aynı şeyi yapar! Fark convention'da:
// hasRole() → Geniş rol tanımları için (ADMIN, USER, EDITOR)
// hasAuthority() → Granüler izinler için (USER_CREATE, REPORT_EXPORT)formLogin — Form Tabanlı Giriş
http.formLogin(form -> form
// Custom login sayfası (varsayılan: Spring'in otomatik ürettiği /login)
.loginPage("/login")
// Form'daki input name'leri (varsayılan: username, password)
.usernameParameter("email")
.passwordParameter("pass")
// Login form'unun POST edildiği URL (varsayılan: /login POST)
.loginProcessingUrl("/perform-login")
// Başarılı giriş sonrası yönlendirme
.defaultSuccessUrl("/dashboard") // Son ziyaret edilen sayfaya dön, yoksa dashboard
.defaultSuccessUrl("/dashboard", true) // Her zaman dashboard'a git
// Custom başarı handler'ı — role göre farklı sayfalara yönlendir
.successHandler((request, response, authentication) -> {
String role = authentication.getAuthorities().toString();
if (role.contains("ADMIN")) {
response.sendRedirect("/admin/dashboard");
} else if (role.contains("EDITOR")) {
response.sendRedirect("/editor/workspace");
} else {
response.sendRedirect("/user/dashboard");
}
})
// Custom başarısızlık handler'ı
.failureHandler((request, response, exception) -> {
String errorMessage;
if (exception instanceof BadCredentialsException) {
errorMessage = "Geçersiz kullanıcı adı veya şifre";
} else if (exception instanceof DisabledException) {
errorMessage = "Hesabınız devre dışı bırakılmış";
} else if (exception instanceof LockedException) {
errorMessage = "Hesabınız kilitlenmiş";
} else {
errorMessage = "Giriş başarısız: " + exception.getMessage();
}
request.getSession().setAttribute("error", errorMessage);
response.sendRedirect("/login?error");
})
.permitAll() // Login sayfasının kendisi herkese açık olmalı!
);💡 İpucu:
.permitAll()çağrısı, login sayfasının kendisini ve login processing URL'ini herkese açar. Bu olmadan kullanıcı login sayfasına bile erişemez — sonsuz yönlendirme döngüsü oluşur!
logout — Çıkış Yapılandırması
http.logout(logout -> logout
.logoutUrl("/logout") // Logout URL'i (POST)
.logoutRequestMatcher( // GET ile logout (form gerektirmez)
new AntPathRequestMatcher("/logout", "GET"))
.logoutSuccessUrl("/login?logout") // Çıkış sonrası yönlendirme
.invalidateHttpSession(true) // Session'ı geçersiz kıl
.deleteCookies("JSESSIONID", "remember-me") // Cookie'leri sil
.clearAuthentication(true) // Authentication'ı temizle
.addLogoutHandler((request, response, auth) -> {
// Custom logout işlemi — audit log yazma
if (auth != null) {
auditService.log("LOGOUT", auth.getName(), request.getRemoteAddr());
}
})
.logoutSuccessHandler((request, response, auth) -> {
// Custom başarı handler — JSON yanıt (API için)
response.setContentType("application/json");
response.getWriter().write("{\"message\": \"Başarıyla çıkış yapıldı\"}");
response.setStatus(200);
})
);Statik Kaynakları Güvenlikten Muaf Tutma
CSS, JavaScript ve görsel dosyaları için güvenlik kontrolü gereksizdir:
// Yöntem 1: requestMatchers ile permitAll (önerilen)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**",
"/favicon.ico", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.build();
}
// Yöntem 2: WebSecurityCustomizer ile tamamen bypass
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.requestMatchers("/css/**", "/js/**", "/images/**");
}`permitAll()` vs `ignoring()` farkı:
permitAll()→ İsteği filtre zincirinden geçirir ama yetkilendirme kontrolünde serbest bırakır. Güvenlik header'ları eklenir.ignoring()→ İsteği filtre zincirine hiç sokmaz — daha performanslıdır ama güvenlik header'ları da eklenmez.
💡 İpucu: Statik kaynaklar için
ignoring()daha performanslıdır. Ancak güvenlik header'larının (Content-Security-Policy, X-Frame-Options) eklenmesini istiyorsanızpermitAll()kullanın.
Birden Fazla SecurityFilterChain
Farklı URL grupları için farklı güvenlik politikaları uygulamak yaygın bir ihtiyaçtır — özellikle hem API hem web arayüzü olan uygulamalarda:
@Configuration
@EnableWebSecurity
public class MultiChainConfig {
// ─── Zincir 1: REST API ──────────────────────────
@Bean
@Order(1)
public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(s -> s
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(csrf -> csrf.disable()) // API'ler için CSRF kapalı
.httpBasic(Customizer.withDefaults()) // veya JWT
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, authEx) -> {
res.setContentType("application/json");
res.setStatus(401);
res.getWriter().write(
"{\"error\": \"Unauthorized\", \"message\": \"Authentication required\"}");
})
.accessDeniedHandler((req, res, accessEx) -> {
res.setContentType("application/json");
res.setStatus(403);
res.getWriter().write(
"{\"error\": \"Forbidden\", \"message\": \"Insufficient privileges\"}");
})
)
.build();
}
// ─── Zincir 2: Web Sayfaları ─────────────────────
@Bean
@Order(2)
public SecurityFilterChain webChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/register", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.rememberMe(remember -> remember
.key("uniqueAndSecret")
.tokenValiditySeconds(604800) // 7 gün
)
.build();
}
}Bu yapılandırmada:
API: Stateless (session yok), CSRF kapalı, HTTP Basic/JWT auth, hata yanıtları JSON
Web: Session tabanlı, CSRF aktif, form login, hata yönlendirmesi HTML sayfasına
Custom Login Sayfası (Thymeleaf)
<!-- templates/login.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Giriş Yap</title>
<link rel="stylesheet" th:href="@{/css/style.css}"/>
</head>
<body>
<div class="login-container">
<h2>Giriş Yap</h2>
<div th:if="${param.error}" class="alert alert-danger">
<p th:text="${session.error != null ? session.error : 'Geçersiz kullanıcı adı veya şifre!'}"></p>
</div>
<div th:if="${param.logout}" class="alert alert-success">
<p>Başarıyla çıkış yaptınız.</p>
</div>
<form th:action="@{/login}" method="post">
<!-- CSRF token Thymeleaf tarafından otomatik eklenir -->
<div class="form-group">
<label for="username">Kullanıcı Adı:</label>
<input type="text" id="username" name="username"
required autofocus placeholder="Kullanıcı adınız"/>
</div>
<div class="form-group">
<label for="password">Şifre:</label>
<input type="password" id="password" name="password"
required placeholder="Şifreniz"/>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="remember-me"/> Beni hatırla
</label>
</div>
<button type="submit" class="btn btn-primary">Giriş Yap</button>
</form>
<p>Hesabınız yok mu? <a th:href="@{/register}">Kayıt Ol</a></p>
</div>
</body>
</html>Spring Security, POST /login isteğini otomatik olarak yakalar ve username + password parametrelerini işler. CSRF token'ı Thymeleaf tarafından <form th:action> kullanıldığında otomatik eklenir.
Exception Handling Yapılandırması
Authentication ve authorization hatalarını özelleştirmek:
http.exceptionHandling(ex -> ex
// 401 — Authentication başarısız (login gerekli)
.authenticationEntryPoint((request, response, authException) -> {
if (isApiRequest(request)) {
// API istekleri için JSON yanıt
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(
"{\"error\":\"Unauthorized\",\"message\":\"Please login first\"}");
} else {
// Web istekleri için login sayfasına yönlendir
response.sendRedirect("/login");
}
})
// 403 — Authorization başarısız (yetki yok)
.accessDeniedHandler((request, response, accessDeniedException) -> {
if (isApiRequest(request)) {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(
"{\"error\":\"Forbidden\",\"message\":\"Insufficient privileges\"}");
} else {
response.sendRedirect("/access-denied");
}
})
);
private boolean isApiRequest(HttpServletRequest request) {
return request.getRequestURI().startsWith("/api/");
}Method-Level Security
URL tabanlı yetkilendirmenin ötesinde, metot seviyesinde de güvenlik uygulanabilir:
@Configuration
@EnableMethodSecurity // Metot seviyesi güvenliği aktifleştir
public class MethodSecurityConfig { }
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { /* ... */ }
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public UserDto getUser(Long userId) { /* ... */ }
@PostAuthorize("returnObject.email == authentication.name")
public UserDto getCurrentUser() { /* ... */ }
}Özet
SecurityFilterChain bean'i, Spring Security 6+'nin modern yapılandırma yöntemidir.
WebSecurityConfigurerAdapterartık kullanılmıyor.HttpSecurity DSL ile endpoint yetkilendirmesi, form login, logout, exception handling ve daha fazlası Lambda syntax ile yapılandırılır.
requestMatchers kuralları yukarıdan aşağıya değerlendirilir — spesifik kurallar önce,
anyRequest()en sonda olmalıdır.Birden fazla SecurityFilterChain kullanarak API ve web için farklı güvenlik politikaları uygulanabilir: API stateless + JWT, web session + form login.
Custom login sayfası Thymeleaf ile oluşturulur. CSRF token otomatik eklenir.
permitAll()veignoring()farkını bilin: permitAll filtre zincirinden geçirir, ignoring hiç sokmaz.@EnableMethodSecurity ile metot seviyesinde
@PreAuthorize,@PostAuthorizekullanabilirsiniz.Exception handling ile API ve web istekleri için farklı hata yanıtları (JSON vs HTML yönlendirme) verin.
authenticationEntryPoint401,accessDeniedHandler403 hatalarını yönetir.Yapılandırmayı test edin: her endpoint için doğru status kodunu (200, 401, 403) ve doğru yönlendirmeyi doğrulayan integration testler yazın.
AI Asistan
Sorularını yanıtlamaya hazır