Gün 3 / Production Basics / Modül 08
Modül 08 Gün 3 Production Basics ~90 dk

Spring Security Basics

Bu modülde Spring Security’yi “korkutucu config sınıfı” olarak değil, HTTP isteği controller’a ulaşmadan önce çalışan kontrollü bir güvenlik katmanı olarak ele alacağız. Önce authentication ve authorization farkını netleştireceğiz; sonra filter chain, JWT, role-based access ve method-level security kavramlarını MiniBank üzerinde birlikte geliştireceğiz.

Anlatım
55 dk
MiniBank Case
50 dk
Kapanış
5 dk
Branch
lab-08

Spring Security’ye başlamadan önce ortak dil

Security konusu çoğu zaman kavramlar birbirine karıştığı için zor görünür. Bu sözlük, modül boyunca aynı kelimeleri aynı anlamda kullanmamızı sağlar.

Authentication

Kullanıcının kim olduğunu doğrulama işlemidir. Spring Security’de bu işlem başarılı olursa sistem artık request’i yapan kişinin kimliğini bilir.

Authorization

Doğrulanmış kullanıcının hangi işlemi yapabileceğine karar verme işlemidir. Örneğin USER rolü hesap görüntüleyebilirken ADMIN rolü kullanıcı yönetebilir.

Principal

Güvenlik bağlamında işlem yapan kullanıcıyı temsil eder. Genellikle kullanıcı adı, user id veya kullanıcı detayları principal üzerinden taşınır.

GrantedAuthority / Role

Kullanıcıya verilmiş yetki bilgisidir. Spring’de rol kontrolü çoğu zaman ROLE_USER ve ROLE_ADMIN gibi authority değerleriyle yapılır.

SecurityContext

O anki request için authentication bilgisinin tutulduğu yerdir. Controller’a gelindiğinde “bu kullanıcı kim?” sorusunun cevabı bu context üzerinden bulunur.

SecurityFilterChain

HTTP request controller’a gitmeden önce çalışan güvenlik filtreleri zinciridir. Login, token doğrulama, yetki kontrolü ve güvenlik hataları bu zincirde ele alınır.

JWT

Kimlik ve yetki bilgisini imzalı bir token olarak taşıyan yapıdır. API istemcisi bu token’ı genellikle Authorization: Bearer ... header’ı ile gönderir.

Method-Level Security

Yetki kontrolünü sadece URL seviyesinde değil, service method seviyesinde de yapabilmektir. Özellikle kritik business işlemlerinde ikinci güvenlik katmanı sağlar.

Spring Security ne işe yarar?

Spring Security, uygulamana gelen request’lerin kimden geldiğini ve o kişinin ne yapmaya yetkili olduğunu kontrol eden güvenlik katmanıdır.

Normalde bir REST API yazdığında request doğrudan controller method’una gelir gibi düşünürsün. Ancak production uygulamasında bu yeterli değildir. Bazı endpoint’ler herkese açık olabilir, bazıları login gerektirebilir, bazıları ise sadece belirli role sahip kullanıcılara açık olmalıdır. Bu kontrolü her controller method’unun içine if yazarak yapmak hem tekrar üretir hem de güvenlik açıklarına davetiye çıkarır.

Spring Security burada devreye girer ve şunu söyler: “Request controller’a gelmeden önce ben araya gireyim. Önce bu kişinin kim olduğunu anlayayım, sonra bu endpoint’e erişmeye yetkisi var mı karar vereyim.” Bu yüzden Spring Security’yi sadece login ekranı veya JWT üretimi olarak düşünmemek gerekir. Asıl değer, güvenlik kararlarını merkezi, okunabilir ve test edilebilir bir katmanda toplamaktır.

Request gelir
Security filter çalışır
Kimlik doğrulanır
Yetki kontrol edilir
Controller’a geçilir
Response döner
MiniBank bağlantısı

MiniBank’ta bazı endpoint’ler herkese açık olacak: örneğin login. Bazıları login gerektirecek: hesap bilgisi görüntüleme. Bazıları ise sadece ADMIN rolüne açık olacak: kullanıcı veya sistem yönetimi. Bu ayrımı controller içine dağıtmak yerine Security configuration ve method-level security ile yöneteceğiz.

“Sen kimsin?” ve “Ne yapabilirsin?” ayrımı

Spring Security’de en temel ayrım authentication ve authorization ayrımıdır. Bu ayrım netleşmeden JWT, role, filter chain veya method security anlatımı havada kalır.

Authentication

Soru: Sen kimsin?

Kullanıcının kimliğini doğrular. Login sırasında username/password kontrolü, token doğrulama veya external identity provider’dan gelen bilgi bu kapsamdadır.

Basit örnek
Kullanıcı: gokhan
Şifre doğru mu?
Token geçerli mi?
Bu request gerçekten bu kullanıcıdan mı geliyor?

Authorization

Soru: Ne yapmaya yetkin var?

Kullanıcının erişmek istediği endpoint veya method için gerekli yetkiye sahip olup olmadığını kontrol eder.

Basit örnek
Bu kullanıcı /api/accounts/me endpoint'ine girebilir mi?
Bu kullanıcı ADMIN işlemi yapabilir mi?
Transfer onaylama yetkisi var mı?

Günlük kodda nasıl görünür?

Authentication başarılı olduğunda Spring Security bir Authentication objesi oluşturur ve bunu SecurityContext içine koyar. Authorization aşamasında ise bu Authentication içindeki authority/role bilgilerine bakılarak karar verilir.

Controller içinde kullanıcı bilgisine erişme
@GetMapping("/api/accounts/me")
public AccountResponse me(Authentication authentication) {
    String username = authentication.getName();
    return accountQueryService.findAccountOf(username);
}
Senior dikkat

Authentication bilgisinin var olması, kullanıcının her şeyi yapabileceği anlamına gelmez. “Login olmuş kullanıcı” ile “bu işlemi yapmaya yetkili kullanıcı” ayrı kavramlardır. Production’da birçok yetki açığı, bu iki kavramın karıştırılmasından doğar.

Request controller’a gelmeden önce nereden geçer?

Spring Security’nin kalbi filter chain’dir. Controller, service veya repository’den önce HTTP request’in geçtiği güvenlik koridoru gibi düşünebilirsin.

Servlet tabanlı Spring Boot uygulamasında Spring Security, servlet filter mekanizmasını kullanır. Bu zincirde birden fazla filter bulunabilir: token okuyan filter, username/password authentication yapan filter, exception handling filter, authorization filter gibi. Biz çoğu zaman bütün filter’ları tek tek yazmayız; SecurityFilterChain bean’i ile hangi request’in hangi kurala tabi olduğunu tanımlarız.

HTTP Request
Servlet Filter
Spring Security Chain
Authentication
Authorization
Controller

Modern Spring Boot Security config nasıl görünür?

Spring Boot 3 / Spring Security 6+ tarafında eski WebSecurityConfigurerAdapter yaklaşımı yerine genellikle SecurityFilterChain bean’i tanımlanır. Bu yapı daha okunabilir ve daha composition-friendly bir model sunar.

SecurityConfig.java
package com.definex.minibank.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(
            HttpSecurity http,
            JwtAuthenticationFilter jwtAuthenticationFilter
    ) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/**", "/actuator/health").permitAll()
                        .requestMatchers(HttpMethod.GET, "/api/accounts/**").hasAnyRole("USER", "ADMIN")
                        .requestMatchers("/api/admin/**").hasRole("ADMIN")
                        .anyRequest().authenticated()
                )
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .build();
    }

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

Bu config ne söylüyor?

permitAll()

Login ve health gibi endpoint’ler token gerektirmeden çağrılabilir. Herkese açık olması gereken endpoint’leri bilinçli şekilde burada listeleriz.

authenticated()

Diğer endpoint’ler için kimlik doğrulama gerekir. Kullanıcı bilinmiyorsa request controller’a ulaşmadan 401 döner.

hasRole("ADMIN")

Sadece ADMIN rolüne sahip kullanıcıların erişebileceği endpoint’leri tanımlar. Role kontrolü merkezi olarak yapılır.

STATELESS

JWT kullanan REST API’lerde sunucu tarafında session tutmak istemeyiz. Her request kendi token’ı ile kimliğini tekrar kanıtlar.

CSRF notu

Bu eğitimde stateless JWT tabanlı API akışı üzerinden ilerlediğimiz için CSRF’i kapatıyoruz. Browser session ve form-login tabanlı uygulamalarda CSRF konusu ayrıca ele alınmalıdır. Bu karar “her projede kapatılır” anlamına gelmez; mimariye göre değerlendirilir.

JWT ile stateless API güvenliği

JWT, login olmuş kullanıcının kimlik ve yetki bilgisini her request’te taşıyabilmesi için kullanılan imzalı token modelidir.

Session tabanlı uygulamalarda kullanıcı login olduktan sonra sunucu tarafında session tutulur. REST API ve mikroservis mimarisinde ise çoğu zaman stateless yaklaşım tercih edilir. Bu durumda sunucu her kullanıcı için session saklamaz; istemci her request’te token gönderir. Spring Security de bu token’ı okuyup doğrular, ardından SecurityContext’i oluşturur.

Login request
Şifre kontrolü
JWT üretilir
Bearer token gönderilir
Filter token’ı doğrular
SecurityContext oluşur

Bearer token request’te nasıl taşınır?

HTTP Header
GET /api/accounts/me HTTP/1.1
Host: localhost:8080
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Training için sade JWT yaklaşımı

Bu eğitimde external identity provider veya authorization server kurmayacağız. MiniBank içinde sade bir JwtService ve JwtAuthenticationFilter yazacağız. Ama production’da kurumsal sistemler çoğu zaman OAuth2/OpenID Connect, merkezi identity provider ve resource server konfigürasyonu ile çalışır.

JwtAuthenticationFilter.java — eğitim skeleton
package com.definex.minibank.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;

    public JwtAuthenticationFilter(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 ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = header.substring(7);
        AuthenticatedUser user = jwtService.parseAndValidate(token);

        UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(
                        user.username(),
                        null,
                        user.authorities()
                );

        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
    }
}
Önemli ayrım

JWT üretmek authentication’ın sadece bir parçasıdır. Asıl güvenlik, token’ın güvenilir şekilde imzalanması, süresinin kontrol edilmesi, doğru secret/key yönetimi, yetkilerin doğru taşınması ve filter chain’de doğru noktada doğrulanmasıyla oluşur.

PasswordEncoder neden gerekli?

Şifreleri düz metin olarak saklamak production’da kabul edilemez. Spring Security’de şifre doğrulama için PasswordEncoder kullanılır. Eğitimde BCryptPasswordEncoder göstereceğiz. Böylece login sırasında gelen ham şifre, saklanan hash ile güvenli şekilde karşılaştırılır.

PasswordEncoder kullanımı
boolean matches = passwordEncoder.matches(rawPassword, storedPasswordHash);

if (!matches) {
    throw new InvalidCredentialsException();
}

Yetki kontrolünü service method seviyesine taşımak

URL seviyesinde güvenlik önemlidir; ama bazı business işlemleri için service method seviyesinde de yetki kontrolü yapmak daha güvenlidir.

Örneğin /api/admin/reports endpoint’i zaten ADMIN gerektiriyor olabilir. Ama aynı rapor üretme method’u başka bir controller’dan, scheduled job’dan veya ileride eklenecek farklı bir akıştan çağrılabilir. Kritik business method’un kendi üzerinde de güvenlik kuralı olması, güvenlik niyetini koda daha yakın taşır.

Method security örneği
@Service
public class AdminReportService {

    @PreAuthorize("hasRole('ADMIN')")
    public AdminReport generateDailyRiskReport() {
        return new AdminReport();
    }
}

@EnableMethodSecurity aktif olduğunda @PreAuthorize gibi annotation’lar devreye girer. Bu da bize URL güvenliği ile method güvenliğini birlikte kullanma imkanı verir.

URL-level security

Request controller’a gelmeden önce çalışır. Endpoint bazlı erişim kuralları için uygundur.

Method-level security

Service method çağrılmadan önce çalışır. Kritik business operasyonlarını korumak için uygundur.

Senior dikkat

Method-level security de çoğu zaman proxy mekanizması üzerinden çalışır. AOP modülünde gördüğümüz gibi, proxy bypass edilen çağrılarda beklenen davranışlar şaşırtıcı olabilir. Bu yüzden security, AOP ve transaction konuları birbirinden kopuk düşünülmemelidir.

MiniBank Case 08 — JWT Security & Role-Based Access

Bu case’de MiniBank API’sine stateless JWT güvenliği ekleyeceğiz. Login endpoint’i herkese açık olacak; hesap endpoint’leri login gerektirecek; admin endpoint’leri ise sadece ADMIN rolüyle erişilebilir olacak. Görev listesi, doğrulama komutları, kod adımları ve kabul kriterleri Lab Dashboard'da.

Başlangıç
lab-08-start
Tamamlanmış
lab-08-complete
Checkpoint kuralı

Lab sırasında geride kalırsan lab-08-complete branch'ine geçip herkesle aynı checkpoint'ten devam edebilirsin.

Bu modülden akılda kalması gerekenler

1

Authentication ve authorization farklıdır.

Önce kullanıcının kim olduğu anlaşılır, sonra ne yapmaya yetkili olduğu kontrol edilir.

2

Spring Security controller’dan önce çalışır.

Request güvenlik filter chain’den geçmeden controller method’una ulaşmaz.

3

JWT stateless API güvenliği için sık kullanılan bir modeldir.

Client her request’te bearer token gönderir; server token’ı doğrulayarak SecurityContext oluşturur.

4

Role-based access merkezi tanımlanmalıdır.

Controller içine dağılmış if kontrolleri yerine SecurityFilterChain ve method-level security tercih edilmelidir.

5

Security tasarımı business tasarımıyla birlikte düşünülür.

Hangi endpoint public, hangisi authenticated, hangisi admin-only olacak kararı API contract’ın parçasıdır.

5 soru ile hızlı kontrol

1. Authentication ve authorization arasındaki fark nedir?

Authentication kullanıcının kim olduğunu doğrular. Authorization ise doğrulanmış kullanıcının belirli bir işlemi yapmaya yetkili olup olmadığını kontrol eder.

2. SecurityFilterChain ne işe yarar?

HTTP request controller’a ulaşmadan önce güvenlik filtrelerinden geçirir. Public endpoint, authenticated endpoint ve role-based access kuralları burada tanımlanır.

3. JWT request içinde nasıl gönderilir?

Genellikle Authorization: Bearer <token> header’ı ile gönderilir.

4. 401 ve 403 farkı nedir?

401 kimlik doğrulama yok veya geçersiz demektir. 403 kullanıcı doğrulanmıştır ama istenen işlem için yetkisi yoktur.

5. Method-level security neden kullanılır?

Kritik business method’larını sadece URL seviyesinde değil, service method seviyesinde de korumak için kullanılır.