Eğitim Portalı/Spring Boot/Spring Security
Spring Boot03-spring-boot/04-spring-security

Spring Security

Bir uygulamayı dünyaya açtığın an, "kimsin?" ve "ne yapmaya yetkilisin?" sorularını cevaplamak zorundasın. Spring Security, kimlik doğrulama (authentication) ve yetkilendirme (authorization) için Java dünyasının fiili…

Spring Security

Bir uygulamayı dünyaya açtığın an, "kimsin?" ve "ne yapmaya yetkilisin?" sorularını cevaplamak zorundasın. Spring Security, kimlik doğrulama (authentication) ve yetkilendirme (authorization) için Java dünyasının fiili standardıdır. Güçlüdür ama çok katmanlıdır; bu yüzden adım adım, çalışan örneklerle ilerleyeceğiz: parola güvenliği ve temel kimlik doğrulamadan, rol bazlı erişime ve modern token tabanlı (JWT) güvenliğe.

İki kavramı baştan ayır: authentication "kim olduğunu" doğrular (401 = doğrulanmadın), authorization "neye yetkin olduğunu" denetler (403 = yetkin yok).

Parola güvenliği ve kimlik doğrulama

İlk kural: parolalar asla düz metin saklanmaz. Tek yönlü, tuzlanmış (salted) bir hash fonksiyonuyla saklanır. Spring'in önerdiği BCryptPasswordEncoder bunu yapar; ilginç biçimde aynı parola her hash'lemede farklı çıktı verir (rastgele tuz), ama matches ile doğrulama çalışır:

PasswordEncoder enc = new BCryptPasswordEncoder();
String h1 = enc.encode("sifre123");
enc.matches("sifre123", h1); // true

Kimlik doğrulamayı bir SecurityFilterChain ile yapılandırırsın: hangi yollar açık, hangileri korumalı; kimlik nasıl alınacak (HTTP Basic, form login...):

@Bean SecurityFilterChain chain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(a -> a
            .requestMatchers("/acik").permitAll()
            .anyRequest().authenticated())
        .httpBasic(Customizer.withDefaults());
    return http.build();
}

Kullanıcılar bir UserDetailsService'ten gelir (örnekte bellek-içi; gerçekte veritabanından). Örnek 1 (./Ornek1.java) BCrypt'i ve HTTP Basic ile korumayı gösterir: açık endpoint 200, korumalı endpoint kimliksiz/yanlış parolayla 401, doğru kimlikle 200 döner.

Yetkilendirme: rol bazlı erişim

Kimliği doğrulanmış her kullanıcı her şeyi yapamamalı. Roller (ROLE_USER, ROLE_ADMIN) ile yetkilendirme yaparsın:

.authorizeHttpRequests(a -> a
    .requestMatchers("/yonetici").hasRole("ADMIN")
    .requestMatchers("/kullanici").hasAnyRole("USER", "ADMIN")
    .anyRequest().authenticated())

Örnek 2 (./Ornek2.java) iki kullanıcıyla (USER ve ADMIN) bunu gösterir: ada kullanıcı alanına girer (200) ama yönetici alanına giremez (403 Forbidden); admin her ikisine de erişir. Metot seviyesinde de @PreAuthorize("hasRole('ADMIN')") ile aynı denetim yapılabilir (proxy tabanlı; AOP bölümündeki mantık).

Token tabanlı güvenlik: JWT

Geleneksel oturum (session) tabanlı güvenlikte sunucu, giriş yapan kullanıcının durumunu bellekte tutar. Mikroservis ve ölçeklenen sistemlerde bu zordur. JWT (JSON Web Token) durumsuz (stateless) bir alternatif sunar: kullanıcı giriş yapınca sunucu imzalı bir token verir; istemci sonraki her isteğinde bu token'ı Authorization: Bearer <token> başlığında taşır. Sunucu oturum tutmaz; sadece token'ın imzasını doğrular.

Akış:

  1. POST /giris — kimlik doğrula, başarılıysa imzalı bir JWT döndür.
  2. Her istekte bir filtre Bearer token'ını okur, imzasını doğrular ve SecurityContext'e kimliği yerleştirir.
  3. Korumalı endpoint, geçerli token olmadan 401 döner.
String token = Jwts.builder().subject(kullanici)
        .expiration(new Date(now + 3600_000)).signWith(KEY).compact();
// doğrulama:
Jwts.parser().verifyWith(KEY).build().parseSignedClaims(token).getPayload().getSubject();

Örnek 3 (./Ornek3.java) uçtan uca JWT akışını kurar: /giris token üretir, bir OncePerRequestFilter token'ı doğrular, SecurityFilterChain durumsuz (STATELESS) yapılandırılır. Self-test: giriş → token; token'sız /profil → reddedilir (kimlik yok); token ile → 200.

Güvenlik filtre zinciri (filter chain) mantığı

Spring Security, gelen her isteği bir filtre zincirinden geçirir. Her filtre bir sorumluluk üstlenir (kimlik çözme, yetki denetimi, CSRF, CORS...). Senin eklediğin JWT filtresi de bu zincire katılır (addFilterBefore). İstek, controller'a ulaşmadan önce bu zincirden geçer; bu yüzden güvenlik, iş mantığından tamamen ayrıdır.

İyi uygulama notları

  • Parolayı her zaman BCrypt (veya Argon2) ile hash'le; düz metin/MD5/SHA-1 kullanma.
  • Üretimde kullanıcıları veritabanından (UserDetailsService implementasyonu) yükle.
  • JWT secret'ını güvenli sakla (ortam değişkeni/secret yöneticisi), token'a kısa ömür ver, hassas veri koyma (JWT imzalıdır ama şifreli değildir, içeriği okunabilir).
  • CSRF'i durum bilgisine göre yönet: stateless JWT API'lerde genelde kapatılır; form/oturum tabanlı uygulamalarda açık tutulur.

Özet

Parola güvenliğini (BCrypt) ve kimlik doğrulamayı (Örnek 1), rol bazlı yetkilendirmeyi (Örnek 2) ve durumsuz JWT akışını (Örnek 3) gerçek, çalışan uygulamalarla gördük; 401/403 farkını ve güvenlik filtre zincirini öğrendik. Uygulaman artık koruma altında. Sırada, uygulamayı izlemek ve üretime hazırlamak: Actuator ve üretim araçları.

Kod Örnekleri(3)

Ornek1

ortam gerekir
Ornek1.java
1// Ornek1: Kimlik doğrulama (authentication) — BCrypt parola + HTTP Basic.
2// Korumalı endpoint kimlik doğrulamadan erişilemez (401); doğru kimlikle erişilir (200).
3// Çalıştırma: portal derleyip gömülü Tomcat + Spring Security ile başlatır.
4package com.egitim.springboot.security;
5
6import org.springframework.boot.CommandLineRunner;
7import org.springframework.boot.SpringApplication;
8import org.springframework.boot.autoconfigure.SpringBootApplication;
9import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
10import org.springframework.context.annotation.Bean;
11import org.springframework.http.HttpStatus;
12import org.springframework.security.config.Customizer;
13import org.springframework.security.config.annotation.web.builders.HttpSecurity;
14import org.springframework.security.core.userdetails.User;
15import org.springframework.security.core.userdetails.UserDetails;
16import org.springframework.security.core.userdetails.UserDetailsService;
17import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
18import org.springframework.security.crypto.password.PasswordEncoder;
19import org.springframework.security.provisioning.InMemoryUserDetailsManager;
20import org.springframework.security.web.SecurityFilterChain;
21import org.springframework.web.bind.annotation.GetMapping;
22import org.springframework.web.bind.annotation.RestController;
23import org.springframework.web.client.RestClient;
24
25@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
26public class Ornek1 {
27
28    public static void main(String[] args) { SpringApplication.run(Ornek1.class, args); }
29
30    @RestController
31    static class ApiController {
32        @GetMapping("/acik")  String acik()  { return "Bu endpoint herkese açık."; }
33        @GetMapping("/gizli") String gizli() { return "Gizli veri (yalnızca doğrulanmış kullanıcı)."; }
34    }
35
36    // Parolalar ASLA düz metin saklanmaz; BCrypt ile (tuzlanarak) hash'lenir.
37    @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
38
39    // Bellek-içi kullanıcı (gerçekte veritabanından gelir).
40    @Bean
41    UserDetailsService users(PasswordEncoder enc) {
42        UserDetails ada = User.withUsername("ada").password(enc.encode("sifre123")).roles("USER").build();
43        return new InMemoryUserDetailsManager(ada);
44    }
45
46    // Güvenlik kuralları: /acik serbest, geri kalan her şey kimlik doğrulama ister; HTTP Basic.
47    @Bean
48    SecurityFilterChain chain(HttpSecurity http) throws Exception {
49        http
50                .csrf(c -> c.disable())
51                .authorizeHttpRequests(a -> a
52                        .requestMatchers("/acik").permitAll()
53                        .anyRequest().authenticated())
54                .httpBasic(Customizer.withDefaults());
55        return http.build();
56    }
57
58    @Bean
59    CommandLineRunner selfTest(PasswordEncoder enc) {
60        return args -> {
61            System.out.println("\n================ KİMLİK DOĞRULAMA (BCrypt + Basic) ================");
62            // BCrypt: aynı parola HER hash'lemede FARKLI çıktı verir (rastgele tuz), ama matches doğrular.
63            String h1 = enc.encode("sifre123"), h2 = enc.encode("sifre123");
64            System.out.println("hash1: " + h1);
65            System.out.println("hash2: " + h2 + "  (farklı! tuz sayesinde)");
66            System.out.println("matches('sifre123', hash1): " + enc.matches("sifre123", h1));
67
68            RestClient c = RestClient.create("http://localhost:8080");
69            System.out.println("\n/acik  (kimliksiz)              -> " + durum(c, "/acik", null, null));
70            System.out.println("/gizli (kimliksiz)              -> " + durum(c, "/gizli", null, null) + "  (reddedildi)");
71            System.out.println("/gizli (ada:yanlis)             -> " + durum(c, "/gizli", "ada", "yanlis") + "  (reddedildi)");
72            System.out.println("/gizli (ada:sifre123)           -> " + durum(c, "/gizli", "ada", "sifre123") + "  (kabul)");
73            System.out.println("===================================================================");
74        };
75    }
76
77    static String durum(RestClient c, String uri, String user, String pass) {
78        return c.get().uri(uri)
79                .headers(h -> { if (user != null) h.setBasicAuth(user, pass); })
80                .exchange((req, res) -> HttpStatus.valueOf(res.getStatusCode().value()).toString());
81    }
82}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek1.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek2

ortam gerekir
Ornek2.java
1// Ornek2: Yetkilendirme (authorization) — rol bazlı erişim kontrolü.
2// Aynı kimlikle giriş yapan farklı roldeki kullanıcılar farklı endpoint'lere erişebilir.
3// Çalıştırma: portal derleyip gömülü Tomcat + Spring Security ile başlatır.
4package com.egitim.springboot.security;
5
6import org.springframework.boot.CommandLineRunner;
7import org.springframework.boot.SpringApplication;
8import org.springframework.boot.autoconfigure.SpringBootApplication;
9import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
10import org.springframework.context.annotation.Bean;
11import org.springframework.http.HttpStatus;
12import org.springframework.security.config.Customizer;
13import org.springframework.security.config.annotation.web.builders.HttpSecurity;
14import org.springframework.security.core.userdetails.User;
15import org.springframework.security.core.userdetails.UserDetailsService;
16import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
17import org.springframework.security.crypto.password.PasswordEncoder;
18import org.springframework.security.provisioning.InMemoryUserDetailsManager;
19import org.springframework.security.web.SecurityFilterChain;
20import org.springframework.web.bind.annotation.GetMapping;
21import org.springframework.web.bind.annotation.RestController;
22import org.springframework.web.client.RestClient;
23
24@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
25public class Ornek2 {
26
27    public static void main(String[] args) { SpringApplication.run(Ornek2.class, args); }
28
29    @RestController
30    static class ApiController {
31        @GetMapping("/acik")      String acik()      { return "herkese açık"; }
32        @GetMapping("/kullanici") String kullanici() { return "kullanıcı alanı (USER veya ADMIN)"; }
33        @GetMapping("/yonetici")  String yonetici()  { return "yönetici alanı (yalnızca ADMIN)"; }
34    }
35
36    @Bean PasswordEncoder enc() { return new BCryptPasswordEncoder(); }
37
38    // İki kullanıcı: 'ada' USER rolünde, 'admin' ADMIN rolünde.
39    @Bean
40    UserDetailsService users(PasswordEncoder enc) {
41        return new InMemoryUserDetailsManager(
42                User.withUsername("ada").password(enc.encode("123")).roles("USER").build(),
43                User.withUsername("admin").password(enc.encode("123")).roles("ADMIN").build());
44    }
45
46    // Rol bazlı kurallar: hangi yol hangi role açık?
47    @Bean
48    SecurityFilterChain chain(HttpSecurity http) throws Exception {
49        http
50                .csrf(c -> c.disable())
51                .authorizeHttpRequests(a -> a
52                        .requestMatchers("/acik").permitAll()
53                        .requestMatchers("/yonetici").hasRole("ADMIN")            // sadece ADMIN
54                        .requestMatchers("/kullanici").hasAnyRole("USER", "ADMIN") // USER veya ADMIN
55                        .anyRequest().authenticated())
56                .httpBasic(Customizer.withDefaults());
57        return http.build();
58    }
59
60    @Bean
61    CommandLineRunner selfTest() {
62        return args -> {
63            RestClient c = RestClient.create("http://localhost:8080");
64            System.out.println("\n================ ROL BAZLI YETKİLENDİRME ================");
65            System.out.println("ada(USER)    -> /kullanici : " + durum(c, "/kullanici", "ada", "123") + "  (izin)");
66            System.out.println("ada(USER)    -> /yonetici  : " + durum(c, "/yonetici", "ada", "123") + "  (YASAK)");
67            System.out.println("admin(ADMIN) -> /kullanici : " + durum(c, "/kullanici", "admin", "123") + "  (izin)");
68            System.out.println("admin(ADMIN) -> /yonetici  : " + durum(c, "/yonetici", "admin", "123") + "  (izin)");
69            System.out.println("=========================================================");
70            System.out.println("401 = kim olduğun bilinmiyor (authentication). 403 = kimliğin var ama yetkin yok (authorization).");
71        };
72    }
73
74    static String durum(RestClient c, String uri, String user, String pass) {
75        return c.get().uri(uri).headers(h -> h.setBasicAuth(user, pass))
76                .exchange((req, res) -> HttpStatus.valueOf(res.getStatusCode().value()).toString());
77    }
78}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek2.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek3

ortam gerekir
Ornek3.java
1// Ornek3: JWT (JSON Web Token) ile durumsuz (stateless) güvenlik.
2// /giris kimlik doğrular ve imzalı bir token verir; korumalı endpoint bu token'ı ister.
3// Çalıştırma: portal derleyip gömülü Tomcat + Spring Security + jjwt ile başlatır.
4package com.egitim.springboot.security;
5
6import io.jsonwebtoken.Jwts;
7import io.jsonwebtoken.security.Keys;
8import jakarta.servlet.FilterChain;
9import jakarta.servlet.http.HttpServletRequest;
10import jakarta.servlet.http.HttpServletResponse;
11import org.springframework.boot.CommandLineRunner;
12import org.springframework.boot.SpringApplication;
13import org.springframework.boot.autoconfigure.SpringBootApplication;
14import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
15import org.springframework.context.annotation.Bean;
16import org.springframework.http.HttpStatus;
17import org.springframework.http.MediaType;
18import org.springframework.http.ResponseEntity;
19import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
20import org.springframework.security.config.annotation.web.builders.HttpSecurity;
21import org.springframework.security.config.http.SessionCreationPolicy;
22import org.springframework.security.core.authority.SimpleGrantedAuthority;
23import org.springframework.security.core.context.SecurityContextHolder;
24import org.springframework.security.web.SecurityFilterChain;
25import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
26import org.springframework.web.bind.annotation.*;
27import org.springframework.web.client.RestClient;
28import org.springframework.web.filter.OncePerRequestFilter;
29
30import javax.crypto.SecretKey;
31import java.util.Date;
32import java.util.List;
33import java.util.Map;
34
35@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
36public class Ornek3 {
37
38    public static void main(String[] args) { SpringApplication.run(Ornek3.class, args); }
39
40    // Token'ı imzalamak/doğrulamak için gizli anahtar (HS256 için >= 256 bit).
41    static final SecretKey KEY = Keys.hmacShaKeyFor("egitim-portal-cok-gizli-jwt-anahtari-256bit!".getBytes());
42
43    static String tokenUret(String kullanici) {
44        return Jwts.builder().subject(kullanici)
45                .issuedAt(new Date())
46                .expiration(new Date(System.currentTimeMillis() + 3600_000)) // 1 saat geçerli
47                .signWith(KEY).compact();
48    }
49
50    static String tokenDogrula(String token) { // geçersizse exception fırlatır
51        return Jwts.parser().verifyWith(KEY).build().parseSignedClaims(token).getPayload().getSubject();
52    }
53
54    @RestController
55    static class AuthController {
56        // Basit kullanıcı deposu (gerçekte veritabanı + BCrypt).
57        private final Map<String, String> kullanicilar = Map.of("ada", "123");
58
59        @PostMapping("/giris")
60        public ResponseEntity giris(@RequestBody Map<String, String> body) {
61            String k = body.get("kullanici"), p = body.get("parola");
62            if (kullanicilar.containsKey(k) && kullanicilar.get(k).equals(p)) {
63                return ResponseEntity.ok(tokenUret(k)); // token döndür
64            }
65            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Geçersiz kimlik");
66        }
67
68        @GetMapping("/profil")
69        public String profil() {
70            // SecurityContext'teki (JWT filtresinin koyduğu) kullanıcı.
71            return "Profil sahibi: " + SecurityContextHolder.getContext().getAuthentication().getName();
72        }
73    }
74
75    // Her istekte Authorization: Bearer <token> başlığını okuyup doğrulayan filtre.
76    static class JwtFilter extends OncePerRequestFilter {
77        @Override
78        protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
79                throws java.io.IOException, jakarta.servlet.ServletException {
80            String header = req.getHeader("Authorization");
81            if (header != null && header.startsWith("Bearer ")) {
82                try {
83                    String kullanici = tokenDogrula(header.substring(7));
84                    var auth = new UsernamePasswordAuthenticationToken(
85                            kullanici, null, List.of(new SimpleGrantedAuthority("ROLE_USER")));
86                    SecurityContextHolder.getContext().setAuthentication(auth); // doğrulandı
87                } catch (Exception e) {
88                    // geçersiz token -> kimlik atanmaz, istek korumalı endpoint'te 401 alır
89                }
90            }
91            chain.doFilter(req, res);
92        }
93    }
94
95    @Bean
96    SecurityFilterChain chain(HttpSecurity http) throws Exception {
97        http
98                .csrf(c -> c.disable())
99                // Stateless: sunucu oturum (session) tutmaz; her istek token taşır.
100                .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
101                .authorizeHttpRequests(a -> a
102                        .requestMatchers("/giris").permitAll()
103                        .anyRequest().authenticated())
104                .addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class);
105        return http.build();
106    }
107
108    @Bean
109    CommandLineRunner selfTest() {
110        return args -> {
111            RestClient c = RestClient.create("http://localhost:8080");
112            System.out.println("\n================ JWT (durumsuz güvenlik) ================");
113
114            // 1) Giriş -> token al
115            String token = c.post().uri("/giris").contentType(MediaType.APPLICATION_JSON)
116                    .body("{\"kullanici\":\"ada\",\"parola\":\"123\"}").retrieve().body(String.class);
117            System.out.println("POST /giris -> token alındı: " + token.substring(0, 30) + "...");
118
119            // 2) Token'sız korumalı endpoint -> 401
120            String tokensiz = c.get().uri("/profil")
121                    .exchange((req, res) -> HttpStatus.valueOf(res.getStatusCode().value()).toString());
122            System.out.println("GET /profil (token'sız)  -> " + tokensiz + "  (reddedildi)");
123
124            // 3) Token ile -> 200
125            String cevap = c.get().uri("/profil").header("Authorization", "Bearer " + token)
126                    .retrieve().body(String.class);
127            System.out.println("GET /profil (token ile)  -> 200 OK | " + cevap);
128            System.out.println("=========================================================");
129            System.out.println("JWT: sunucu oturum tutmaz; kimlik, imzalı token'da taşınır (mikroservis dostu).");
130        };
131    }
132}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek3.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.