← Kursa Dön
📄 Text · 30 min

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ılara

  • Geri 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ı: requestMatchers kuralları 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ız permitAll() 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. WebSecurityConfigurerAdapter artı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() ve ignoring() farkını bilin: permitAll filtre zincirinden geçirir, ignoring hiç sokmaz.

  • @EnableMethodSecurity ile metot seviyesinde @PreAuthorize, @PostAuthorize kullanabilirsiniz.

  • Exception handling ile API ve web istekleri için farklı hata yanıtları (JSON vs HTML yönlendirme) verin. authenticationEntryPoint 401, accessDeniedHandler 403 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.