Spring Boot OAuth2 ve JWT
Modern API'ler durumsuz (stateless) kimlik doğrulama kullanır: sunucu oturum (session) tutmaz; istemci her istekte kim olduğunu kanıtlayan bir token taşır. Bu token genelde bir JWT (JSON Web Token)'dır ve OAuth2 akışl…
Spring Boot OAuth2 ve JWT
Modern API'ler durumsuz (stateless) kimlik doğrulama kullanır: sunucu oturum (session) tutmaz; istemci her istekte kim olduğunu kanıtlayan bir token taşır. Bu token genelde bir JWT (JSON Web Token)'dır ve OAuth2 akışlarının merkezindedir. Bu konu, JWT'nin nasıl çalıştığını çalışan bir örnekle gösterir ve OAuth2'nin daha geniş resmini açıklar. (Temel güvenlik — BCrypt, roller — topic 04'tedir; bu konu token tabanlı durumsuz kimliğe odaklanır.)
JWT nedir?
JWT, üç parçadan oluşan, imzalı bir token'dır: header.payload.signature (Base64URL kodlu, topic 71):
- Header: Algoritma (örn. HS256).
- Payload (claims): Veri —
sub(kim),exp(son kullanma), roller, özel iddialar. - Signature: Gizli anahtarla imza. Token kurcalanırsa imza doğrulaması bozulur.
JWT şifreli değildir, imzalıdır — içeriği herkes okuyabilir (Base64), ama değiştiremez (imza). Bu yüzden hassas veri koyma; sadece kimlik/yetki bilgisi.
JWT üretme ve doğrulama (jjwt)
SecretKey anahtar = Keys.hmacShaKeyFor(gizliByte); // HS256 için >=256 bit // Üret (login sırasında) String token = Jwts.builder() .subject("ada").claim("roller", List.of("USER")) .issuedAt(new Date()).expiration(new Date(now + 3600_000)) .signWith(anahtar).compact(); // Doğrula (her istekte) Claims c = Jwts.parser().verifyWith(anahtar).build() .parseSignedClaims(token).getPayload(); // imza + süre geçersizse exception c.getSubject();
Örnek 1 (./Ornek1.java) bir /login (token üretir) ve korumalı /profil (token doğrular) ucu
kurar; self-test: login → token ile erişim (200), token olmadan ve sahte token ile reddedilir
(401).
Akış (bearer token)
1. POST /login (kullanıcı/parola) -> sunucu JWT üretir, döndürür 2. İstemci token'ı saklar (genelde bellek/secure storage) 3. Her istekte: Authorization: Bearer <token> 4. Sunucu token'ı doğrular (imza + süre) -> kim olduğunu bilir (DB'ye gitmeden, stateless)
OAuth2: daha büyük resim
JWT bir token formatıdır; OAuth2 ise token'ların nasıl alınıp kullanılacağını tanımlayan bir yetkilendirme çerçevesidir. Roller:
- Authorization Server: Token üreten sunucu (Google, Auth0, Keycloak veya Spring Authorization Server).
- Resource Server: Token'ı doğrulayıp korunan kaynağı sunan API'n.
- Client: Token alıp kullanan uygulama.
- Access token / Refresh token: Kısa ömürlü erişim + uzun ömürlü yenileme; scope'lar yetki kapsamını belirler.
Gerçek Spring kurulumunda bunu elle yapmazsın; spring-boot-starter-oauth2-resource-server ile
birkaç satırda kurulur:
// build.gradle: spring-boot-starter-oauth2-resource-server @Bean SecurityFilterChain chain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(a -> a.anyRequest().authenticated()) .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())); // JWT'yi otomatik doğrular return http.build(); }
spring.security.oauth2.resourceserver.jwt.issuer-uri: https://accounts.google.com
Not: OAuth2 resource server ve Google Sign-In ek bağımlılık + dış sağlayıcı (Google/Auth0) gerektirir; bu portalda çalıştırılamaz. Örnek 1, JWT mekaniğini jjwt ile gerçek çalıştırarak gösterir; yukarıdaki Spring kurulumu üretimde tercih edilen yoldur.
Güvenlik pratikleri
- Gizli anahtarı koruma: Ortam değişkeni/secret yöneticisi; koda gömme. Asimetrik (RS256) imzada özel anahtar yalnızca authorization server'da.
- Kısa ömür + refresh: Access token kısa (dakikalar), yenileme için refresh token.
- HTTPS şart: Token açık metinde taşınır; araya girme (MITM) için HTTPS zorunlu.
- Hassas veri koyma: Payload herkesçe okunur (sadece imzalı).
expdoğrula: Süresi geçmiş token reddedilmeli (jjwt bunu otomatik yapar).
Özet
Durumsuz kimlik için JWT'yi öğrendik: yapısı (header/payload/signature, imzalı ama şifresiz),
jjwt ile üretme/doğrulama ve bearer-token akışı (Örnek 1 — login→token→korumalı erişim, 401
senaryoları); OAuth2'nin rolleri (authorization/resource server, access/refresh token, scope) ve
Spring'in oauth2-resource-server ile gerçek kurulumu; güvenlik pratikleri. Sırada, Spring Boot'un
anotasyon ekosistemine genel bakış: annotations.
▶ Kod Örnekleri(1)
Ornek1
ortam gerekir1// Ornek1: JWT (JSON Web Token) ile durumsuz (stateless) kimlik — token üret, doğrula, koru.
2// OAuth2'nin "bearer token" modelinin çekirdeği. (jjwt kütüphanesi classpath'te mevcut.)
3// Çalıştırma: portal gömülü Tomcat ile başlatır, self-test çıktısını alır.
4package com.egitim.springboot.jwt;
5
6import io.jsonwebtoken.Claims;
7import io.jsonwebtoken.Jwts;
8import io.jsonwebtoken.security.Keys;
9import org.springframework.boot.CommandLineRunner;
10import org.springframework.boot.SpringApplication;
11import org.springframework.boot.autoconfigure.SpringBootApplication;
12import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
13import org.springframework.context.annotation.Bean;
14import org.springframework.http.ResponseEntity;
15import org.springframework.web.bind.annotation.*;
16import org.springframework.web.client.RestClient;
17
18import javax.crypto.SecretKey;
19import java.util.Date;
20import java.util.List;
21import java.util.Map;
22
23@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
24public class Ornek1 {
25
26 public static void main(String[] args) { SpringApplication.run(Ornek1.class, args); }
27
28 // İmzalama anahtarı (gerçekte gizli, ortam değişkeninden gelir; HS256 için >=256 bit).
29 static final SecretKey ANAHTAR = Keys.hmacShaKeyFor(
30 "egitim-portali-cok-gizli-jwt-anahtari-256bit!!".getBytes());
31
32 @RestController
33 static class AuthController {
34
35 // /login: kimlik doğrulanır (burada basit), bir JWT üretilir ve döndürülür.
36 @PostMapping("/login")
37 public Map<String, String> login(@RequestParam String kullanici) {
38 String token = Jwts.builder()
39 .subject(kullanici) // kime ait (sub)
40 .claim("roller", List.of("USER")) // özel iddia (claim)
41 .issuedAt(new Date()) // ne zaman üretildi
42 .expiration(new Date(System.currentTimeMillis() + 3600_000)) // 1 saat geçerli
43 .signWith(ANAHTAR) // imzala (kurcalanırsa doğrulama bozulur)
44 .compact();
45 return Map.of("token", token);
46 }
47
48 // /profil: korumalı uç — geçerli bir 'Authorization: Bearer <token>' ister.
49 @GetMapping("/profil")
50 public ResponseEntity<?> profil(@RequestHeader(value = "Authorization", required = false) String auth) {
51 if (auth == null || !auth.startsWith("Bearer ")) {
52 return ResponseEntity.status(401).body(Map.of("hata", "token yok"));
53 }
54 try {
55 Claims c = Jwts.parser().verifyWith(ANAHTAR).build()
56 .parseSignedClaims(auth.substring(7)).getPayload(); // imza+süre doğrula
57 return ResponseEntity.ok(Map.of("kullanici", c.getSubject(), "roller", c.get("roller")));
58 } catch (Exception e) {
59 return ResponseEntity.status(401).body(Map.of("hata", "geçersiz/expired token"));
60 }
61 }
62 }
63
64 @Bean
65 CommandLineRunner selfTest() {
66 return args -> {
67 RestClient c = RestClient.create("http://localhost:8080");
68 System.out.println("\n========= JWT SELF-TEST =========");
69
70 // 1) Login -> token al
71 Map<?,?> giris = c.post().uri("/login?kullanici=ada").retrieve().body(Map.class);
72 String token = (String) giris.get("token");
73 System.out.println("LOGIN -> token (ilk 30): " + token.substring(0, 30) + "...");
74
75 // 2) Token ile korumalı uca eriş -> 200
76 String profil = c.get().uri("/profil").header("Authorization", "Bearer " + token)
77 .retrieve().body(String.class);
78 System.out.println("PROFİL (token ile) -> " + profil);
79
80 // 3) Token olmadan -> 401
81 try {
82 c.get().uri("/profil").retrieve().body(String.class);
83 } catch (Exception e) {
84 System.out.println("PROFİL (token YOK) -> reddedildi (401)");
85 }
86 // 4) Sahte token -> 401
87 try {
88 c.get().uri("/profil").header("Authorization", "Bearer sahte.token.degeri")
89 .retrieve().body(String.class);
90 } catch (Exception e) {
91 System.out.println("PROFİL (sahte token) -> reddedildi (401)");
92 }
93 System.out.println("=================================");
94 };
95 }
96}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.