← Kursa Dön
📄 Text · 25 min

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?

  1. 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.

  2. GPU ile Brute Force: Modern GPU'lar saniyede milyarlarca MD5 hash üretebilir. 8 karakterlik bir şifrenin tüm kombinasyonları saatler içinde denenebilir.

  3. 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ğeri

Strength (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:

StrengthYaklaşık SüreKullanım
4~1msTest (çok hızlı ama güvensiz)
10~100msGeliştirme (varsayılan)
12~400msProduction (önerilen)
14~1.5sYüksek güvenlik gereksinimleri
16~6sAşı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, DelegatingPasswordEncoder prefix ekler ({bcrypt}$2a$10$...) ve ileride daha uzun hash algoritmasına geçebilirsiniz. `VARCHAR(255)` güvenli seçimdir.

Güvenlik Kuralları Özeti

KuralAçıklama
Asla düz metin saklamaHer zaman hash kullanın
MD5/SHA-1 kullanmayınÇok hızlı, kolayca kırılır
BCrypt varsayılan seçimGüvenilir, yaygın, yeterli
Argon2 en güçlüBellek-yoğun, GPU'ya dirençli
Salt otomatikBCrypt/Argon2 kendi salt'larını üretir
matches() kullanınencode().equals() yazmayın — salt her seferinde farklı
Cost factor ayarlayınProduction'da BCrypt en az 12 strength
Şifre kuralları uygulayınMinimum uzunluk, karmaşıklık, yaygın şifre kontrolü
DelegatingPasswordEncoderFarklı 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.