Password Encoding
Şifrelerin güvenli saklanması, bir uygulamanın en kritik güvenlik gereksinimlerinden biridir. Tarih boyunca pek çok büyük veri sızıntısı — LinkedIn (2012, 117 milyon kullanıcı), Adobe (2013, 153 milyon), Yahoo (2013, 3 milyar) — şifrelerin düz metin veya zayıf hash ile saklanmasından kaynaklanmıştır. Spring Security, modern ve güvenli password encoding mekanizmaları sunar.
Bunu bir kasanın kilidi gibi düşünün. Düz metin şifre saklamak, para kasasını açık bırakmaktır. MD5 hash kullanmak, çürük tahtadan bir kapı takmaktır. BCrypt kullanmak ise çelik kapı ve şifreli kilit koymaktır. Aradaki fark, saldırgan veritabanınıza ulaştığında ne kadar sürede şifreleri kırabileceğidir.
Neden Password Hashing?
Şifreler asla düz metin (plaintext) olarak saklanmamalıdır. Veritabanı sızdırıldığında tüm şifreler açığa çıkar. Bunun yerine tek yönlü hash fonksiyonları kullanılır — şifreden hash üretilir ama hash'ten şifre geri elde edilemez:
Şifre: "mySecret123"
↓ Hash Fonksiyonu (BCrypt)
Hash: "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
↓ Geri Dönüşüm
✗ İMKANSIZ (tek yönlü fonksiyon)Neden Basit Hash (MD5, SHA-256) Yetersiz?
Rainbow Table Saldırıları: Yaygın şifrelerin hash değerleri önceden hesaplanmış devasa tablolar mevcuttur. MD5("password") = "5f4dcc3b5aa765d61d8327deb882cf99" — bu hash'i Google'da aratmanız bile yeterli.
GPU ile Brute Force: Modern GPU'lar saniyede milyarlarca MD5 hash üretebilir. 8 karakterlik bir şifrenin tüm kombinasyonları saatler içinde denenebilir.
Salt Yokluğu: Aynı şifreyi kullanan farklı kullanıcılar aynı hash'e sahip olur. Bir kullanıcının şifresi kırıldığında aynı şifreyi kullanan herkes tehlikededir.
// ❌ TEHLİKELİ — MD5 saniyeler içinde kırılır
String hash = DigestUtils.md5Hex("password123");
// "482c811da5d5b4bc6d497ffa98491e38"
// Google'da aratın — şifreyi bulursunuz!
// ❌ TEHLİKELİ — SHA-256 da GPU ile hızlıca kırılır
String hash = DigestUtils.sha256Hex("password123");
// ✅ GÜVENLI — BCrypt her seferinde farklı hash üretir (salt dahili)
String hash1 = new BCryptPasswordEncoder().encode("password123");
// "$2a$10$abc...xyz" — her çağrıda farklı sonuç!
String hash2 = new BCryptPasswordEncoder().encode("password123");
// "$2a$10$def...uvw" — salt farklı olduğu için hash da farklıPasswordEncoder Interface
public interface PasswordEncoder {
// Şifreyi hashle — her çağrıda farklı sonuç (rastgele salt)
String encode(CharSequence rawPassword);
// Raw şifre ile encoded şifreyi karşılaştır
boolean matches(CharSequence rawPassword, String encodedPassword);
// Mevcut hash'in yeniden hash'lenmesi gerekli mi? (migration için)
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}Kritik kural: encode() her çağrıda farklı sonuç üretir (rastgele salt nedeniyle). Bu nedenle şifre doğrulamada doğrudan karşılaştırma yapmak yanlıştır:
// ❌ YANLIŞ — Salt farklı olduğu için her zaman false döner
boolean match = encoder.encode("password").equals(savedHash);
// ✅ DOĞRU — matches() metodu salt'ı hash'ten çıkarıp doğru karşılaştırma yapar
boolean match = encoder.matches("password", savedHash);BCryptPasswordEncoder — Varsayılan ve Önerilen
BCrypt, Spring Security'nin varsayılan ve önerilen password encoder'ıdır. Dahili salt, adaptive cost factor ve 25+ yıllık savaş testinden geçmiş algoritmasıyla güvenilirdir:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // Varsayılan strength: 10
}
// Strength (cost factor) ayarlama
@Bean
public PasswordEncoder strongEncoder() {
return new BCryptPasswordEncoder(12); // 2^12 = 4096 iterasyon
}BCrypt Hash Formatı
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
$2a$ → BCrypt versiyonu (2a, 2b, 2y)
10$ → Cost factor (2^10 = 1024 iterasyon)
N9qo8uLOickgx2ZMRZoMye → Salt (22 karakter, Base64)
IjZAgcfl7p92ldGxad68LJZdL17lhWy → Hash değeriStrength (Cost Factor) Seçimi
Her 1 artış süreyi 2 katına çıkarır. Doğru değer seçimi güvenlik ve performans arasında dengedir:
| Strength | Yaklaşık Süre | Kullanım |
|---|---|---|
| 4 | ~1ms | Test (çok hızlı ama güvensiz) |
| 10 | ~100ms | Geliştirme (varsayılan) |
| 12 | ~400ms | Production (önerilen) |
| 14 | ~1.5s | Yüksek güvenlik gereksinimleri |
| 16 | ~6s | Aşırı — DoS riski yaratır |
| 18+ | ~25s+ | Kullanmayın — login dakikalar sürer |
// Doğru strength'i ölçmek
@Test
void measureBcryptPerformance() {
PasswordEncoder encoder = new BCryptPasswordEncoder(12);
long start = System.currentTimeMillis();
encoder.encode("testPassword");
long duration = System.currentTimeMillis() - start;
System.out.println("BCrypt(12) süresi: " + duration + "ms");
// 200-500ms arası ideal — kullanıcı fark etmez, saldırgan yavaşlar
}⚠️ Dikkat: Çok yüksek strength değerleri (16+) DoS riski yaratır. Bir saldırgan yüzlerce login isteği göndererek sunucunuzu meşgul edebilir — her istek saniyeler sürer.
Argon2PasswordEncoder
Argon2, 2015 Password Hashing Competition kazananıdır. BCrypt'e göre daha modern ve bellek-yoğun saldırılara karşı dayanıklıdır:
@Bean
public PasswordEncoder argon2Encoder() {
return new Argon2PasswordEncoder(
16, // salt length (bytes)
32, // hash length (bytes)
1, // parallelism (thread sayısı)
65536, // memory cost (KB) — 64MB
3 // iterations
);
}Argon2'nin avantajı: hem CPU hem bellek yoğun olmasıdır. GPU/ASIC tabanlı brute force saldırılarını BCrypt'ten daha etkili engeller. GPU'lar çok hızlı hesaplama yapabilir ama sınırlı belleğe sahiptir — Argon2 bu kısıtlamayı kullanır.
BCrypt: CPU yoğun → GPU ile paralelleştirilebilir (daha az güvenli)
Argon2: CPU + RAM yoğun → GPU bellek sınırına takılır (daha güvenli)Ancak Spring Security ekosisteminde BCrypt hâlâ varsayılan ve en yaygın tercihdir — Argon2 desteği daha yenidir ve bazı ortamlarda uyumluluk sorunları olabilir.
SCryptPasswordEncoder
SCrypt de bellek-yoğun bir algoritmadır, Argon2'den eskidir:
@Bean
public PasswordEncoder scryptEncoder() {
return new SCryptPasswordEncoder(
16384, // CPU/memory cost parameter (N)
8, // block size (r)
1, // parallelism (p)
32, // key length
64 // salt length
);
}DelegatingPasswordEncoder: Password Migration
Gerçek dünyada bir uygulama zaman içinde farklı encoding algoritmaları kullanabilir. Başlangıçta MD5, sonra SHA-256, sonra BCrypt kullanılmış olabilir. DelegatingPasswordEncoder, hash'in başındaki {id} prefix'ine bakarak doğru encoder'ı seçer:
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
// Varsayılan: bcrypt
}
// Veya custom yapılandırma
@Bean
public PasswordEncoder customDelegating() {
String defaultEncoder = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder(12));
encoders.put("argon2", new Argon2PasswordEncoder(16, 32, 1, 65536, 3));
encoders.put("scrypt", new SCryptPasswordEncoder(16384, 8, 1, 32, 64));
encoders.put("noop", NoOpPasswordEncoder.getInstance()); // Test için
encoders.put("sha256", new StandardPasswordEncoder()); // Legacy
return new DelegatingPasswordEncoder(defaultEncoder, encoders);
}Hash formatı prefix'e göre doğru encoder seçilir:
{bcrypt}$2a$10$... → BCryptPasswordEncoder
{argon2}$argon2id$... → Argon2PasswordEncoder
{scrypt}$e0801$... → SCryptPasswordEncoder
{noop}plaintext → NoOpPasswordEncoder (düz metin — ASLA production'da!)
{sha256}... → StandardPasswordEncoder (legacy)⚠️ Dikkat:
{noop}prefix'i şifreyi düz metin olarak saklar — sadece test ve geliştirme için kullanın, asla üretimde kullanmayın.
Password Migration Stratejisi — Gradual Migration
Eski sistemdeki SHA-256 hash'lerini BCrypt'e geçirmek için kullanılan strateji. Kullanıcılar giriş yaptıkça şifreleri otomatik olarak yeni algoritmayla yeniden hash'lenir:
@Service
public class PasswordMigrationService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public PasswordMigrationService(UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Bulunamadı: " + username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getAuthorities()
);
}
// Başarılı login sonrası hash'i güncelle
@EventListener
@Transactional
public void onAuthSuccess(AuthenticationSuccessEvent event) {
Authentication auth = event.getAuthentication();
if (auth.getPrincipal() instanceof UserDetails userDetails) {
String currentHash = userDetails.getPassword();
// Hash upgrade gerekli mi? (eski algoritma veya düşük strength)
if (passwordEncoder.upgradeEncoding(currentHash)) {
// Credentials authentication sırasında mevcut — tekrar hash'le
// NOT: Credentials temizlendiği için bu pattern farklı çalışır
// Custom DaoAuthenticationProvider gerekebilir
log.info("Password hash upgraded for user: {}", userDetails.getUsername());
}
}
}
}Gradual migration prensibi: Kullanıcıyı etkilemeden, zaman içinde tüm hash'ler yeni algoritmaya geçer. 6 ay sonra eski algoritmayla kalan hash'ler zorla şifre değiştirme ile yenilenebilir.
Şifre Güvenlik Kuralları
Uygulama seviyesinde şifre güvenlik kuralları uygulamak:
// Custom şifre validator
public class PasswordValidator {
private static final int MIN_LENGTH = 8;
private static final int MAX_LENGTH = 128;
private static final Pattern UPPERCASE = Pattern.compile("[A-Z]");
private static final Pattern LOWERCASE = Pattern.compile("[a-z]");
private static final Pattern DIGIT = Pattern.compile("[0-9]");
private static final Pattern SPECIAL = Pattern.compile("[!@#$%^&*()_+\\-=\\[\\]{}|;':\",./<>?]");
// Yaygın şifre listesi (gerçekte çok daha büyük bir liste kullanın)
private static final Set<String> COMMON_PASSWORDS = Set.of(
"password", "123456", "qwerty", "letmein", "admin", "welcome"
);
public List<String> validate(String password) {
List<String> errors = new ArrayList<>();
if (password.length() < MIN_LENGTH) {
errors.add("Şifre en az " + MIN_LENGTH + " karakter olmalı");
}
if (password.length() > MAX_LENGTH) {
errors.add("Şifre en fazla " + MAX_LENGTH + " karakter olmalı");
}
if (!UPPERCASE.matcher(password).find()) {
errors.add("En az bir büyük harf içermeli");
}
if (!LOWERCASE.matcher(password).find()) {
errors.add("En az bir küçük harf içermeli");
}
if (!DIGIT.matcher(password).find()) {
errors.add("En az bir rakam içermeli");
}
if (!SPECIAL.matcher(password).find()) {
errors.add("En az bir özel karakter içermeli");
}
if (COMMON_PASSWORDS.contains(password.toLowerCase())) {
errors.add("Bu şifre çok yaygın, lütfen farklı bir şifre seçin");
}
return errors;
}
}Timing Attack'lara Karşı Koruma
Şifre karşılaştırmasında sıradan String.equals() kullanmak timing attack açığı oluşturur. equals() ilk farklı karakterde durur — saldırgan yanıt süresini ölçerek doğru şifrenin kaç karakterini bildiğini anlayabilir:
// ❌ TEHLİKELİ — Timing attack'a açık
boolean match = inputPassword.equals(storedPassword);
// "aXXXXXXX" → 1ms (ilk karakterde duruyor)
// "pXXXXXXX" → 1ms
// "paXXXXXX" → 2ms (ilk karakter doğru, ikincide durdu)
// Saldırgan süreyi ölçerek şifreyi karakter karakter çözer!
// ✅ GÜVENLI — Spring Security sabit zamanlı karşılaştırma kullanır
// BCryptPasswordEncoder.matches() içinde:
// MessageDigest.isEqual() kullanılır — tüm karakterler karşılaştırılır
boolean match = passwordEncoder.matches(rawPassword, encodedPassword);Spring Security'nin PasswordEncoder.matches() metodu bu saldırıya karşı sabit zamanlı (constant-time) karşılaştırma kullanır. Sonuç ne olursa olsun aynı sürede tamamlanır.
Şifre Değiştirme ve Sıfırlama Akışı
Production uygulamalarında şifre yönetimi sadece encoding değil, tüm yaşam döngüsünü kapsar:
@Service
@RequiredArgsConstructor
public class PasswordService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final PasswordValidator passwordValidator;
// Şifre değiştirme (kullanıcı mevcut şifresini bilir)
@Transactional
public void changePassword(Long userId, String currentPassword,
String newPassword) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found"));
// Mevcut şifreyi doğrula
if (!passwordEncoder.matches(currentPassword, user.getPassword())) {
throw new BadCredentialsException("Mevcut şifre yanlış");
}
// Yeni şifre eski şifreyle aynı olmamalı
if (passwordEncoder.matches(newPassword, user.getPassword())) {
throw new IllegalArgumentException("Yeni şifre eskisiyle aynı olamaz");
}
// Şifre kurallarını kontrol et
List<String> errors = passwordValidator.validate(newPassword);
if (!errors.isEmpty()) {
throw new PasswordPolicyException(errors);
}
// Yeni şifreyi kaydet
user.setPassword(passwordEncoder.encode(newPassword));
user.setPasswordChangedAt(LocalDateTime.now());
userRepository.save(user);
}
// Şifre sıfırlama (token ile — kullanıcı şifresini unutmuş)
@Transactional
public void resetPassword(String token, String newPassword) {
PasswordResetToken resetToken = tokenRepository.findByToken(token)
.orElseThrow(() -> new InvalidTokenException("Geçersiz token"));
if (resetToken.isExpired()) {
throw new InvalidTokenException("Token süresi dolmuş");
}
User user = resetToken.getUser();
user.setPassword(passwordEncoder.encode(newPassword));
user.setPasswordChangedAt(LocalDateTime.now());
userRepository.save(user);
// Token'ı kullanılmış olarak işaretle (tek kullanımlık)
tokenRepository.delete(resetToken);
}
}Password Encoding ve Veritabanı
Veritabanındaki password sütununun yeterli uzunlukta olduğundan emin olun:
-- BCrypt hash'leri yaklaşık 60 karakter uzunluğundadır
-- Argon2 hash'leri daha uzun olabilir
-- DelegatingPasswordEncoder prefix ekler ({bcrypt}...)
-- En az 255 karakter ayırın
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, -- 255 karakter yeterli
-- ...
);⚠️ Dikkat:
VARCHAR(60)BCrypt için yeterli gibi görünse de,DelegatingPasswordEncoderprefix ekler ({bcrypt}$2a$10$...) ve ileride daha uzun hash algoritmasına geçebilirsiniz. `VARCHAR(255)` güvenli seçimdir.
Güvenlik Kuralları Özeti
| Kural | Açıklama |
|---|---|
| Asla düz metin saklama | Her zaman hash kullanın |
| MD5/SHA-1 kullanmayın | Çok hızlı, kolayca kırılır |
| BCrypt varsayılan seçim | Güvenilir, yaygın, yeterli |
| Argon2 en güçlü | Bellek-yoğun, GPU'ya dirençli |
| Salt otomatik | BCrypt/Argon2 kendi salt'larını üretir |
matches() kullanın | encode().equals() yazmayın — salt her seferinde farklı |
| Cost factor ayarlayın | Production'da BCrypt en az 12 strength |
| Şifre kuralları uygulayın | Minimum uzunluk, karmaşıklık, yaygın şifre kontrolü |
| DelegatingPasswordEncoder | Farklı algoritmaları aynı anda destekler, migration kolaylaştırır |
Özet
Şifreler asla düz metin saklanmamalı — hash + salt ile güvenli hale getirilir.
BCrypt, Spring Security'nin varsayılan ve önerilen encoder'ıdır. Production'da strength 12+ kullanın.
Argon2, en modern ve en güvenli algoritmadır — GPU/ASIC saldırılarına karşı bellek-yoğun savunma sağlar.
`matches()` metodu ile şifre doğrulayın,
encode().equals()asla kullanmayın.DelegatingPasswordEncoder ile eski ve yeni hash algoritmalarını aynı anda destekleyin. Gradual migration ile zaman içinde geçiş yapın.
Cost factor (strength) güvenlik ve performans dengesini belirler. Çok düşük → güvensiz, çok yüksek → DoS riski.
Şifre güvenlik kuralları (uzunluk, karmaşıklık, yaygın şifre kontrolü) uygulama seviyesinde de uygulanmalıdır.
AI Asistan
Sorularını yanıtlamaya hazır