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ış:
POST /giris— kimlik doğrula, başarılıysa imzalı bir JWT döndür.- Her istekte bir filtre
Bearertoken'ını okur, imzasını doğrular veSecurityContext'e kimliği yerleştirir. - 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 (
UserDetailsServiceimplementasyonu) 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 gerekir1// 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}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 gerekir1// 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}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 gerekir1// 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}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.