JavaProgramlamaJava Geliştirici

Java'nın yerel serileştirme protokolü içindeki hangi mekanizma, saldırganların sözde bir singleton'ın birden fazla örneğini oluşturmasına olanak tanır ve hangi savunma kancası yöntemleri, seri hale getirme sırasında örnek kontrolünü garanti eder?

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun cevabı

Sorunun tarihi: Java, JDK 1.1 ile birlikte ObjectOutputStream ve ObjectInputStream API'leri aracılığıyla yerel ikili serileştirmeyi tanıttı ve nesne grafiklerinin kalıcılık veya ağ aktarımı için bayt akışlarına düzlendiği bir protokol kurdu. Şartname, yeniden yapılandırma sırasında ObjectInputStream'in hedef nesne için bellekte yer ayırmasını, sun.misc.Unsafe veya doğrudan yansıma kullanarak gerçekleştirmesini zorunlu kılar; bu, yapıcıları tamamen atlatır. Bu tasarım tercihi, örneklemeyi kısıtlamak için özel yapıcılara bağımlı olan singleton deseninin temel prensibiyle doğrudan çelişmektedir.

Sorun: Bir sınıf Serializable'yı uyguladığında, seri hale getirme çerçevesi, yapıcı mantığını çalıştırmadan allocateInstance çağrısını yaparak yeni bir örnek oluşturur. Bir singleton, özel bir yapıcı ve statik bir fabrika aracılığıyla tek varlığını zorunlu kılmak üzere tasarlandığından, bu müdahale, yığın üzerinde ikinci bir ayrı nesne üretir ve kimlik eşitliği garantisini ihlal eder. Sonuç olarak, küresel olması beklenen statik durum, birden fazla örnek arasında parçalanır ve tekil kontrol noktalarına dayanan uygulamalarda tutarsız davranışlara yol açar.

Çözüm: readResolve yöntemi, Serializable sözleşmesinde tanımlanan bir seri hale getirme sonrası kancası olarak görev yaparak, sınıfın seri hale getirilmiş nesneyi, çağrıcıya geri döndürülmeden önce kanonik örnekle değiştirmesine olanak tanır. Tam olarak protected Object readResolve() throws ObjectStreamException imzasına sahip bir yöntem tanımlayarak, geliştiriciler yeniden oluşturulan çoğaltıcıyı engelleyebilir ve statik INSTANCE alanını döndürebilir. Bu değişim, akışın çözümleme sürecinde sorunsuz bir şekilde gerçekleşir ve sahte nesneyi çöp toplama için atanırken singleton bütünlüğünü korur.

public class Configuration implements Serializable { private static final Configuration INSTANCE = new Configuration(); private String dbUrl; private Configuration() { this.dbUrl = System.getenv("DB_URL"); } public static Configuration getInstance() { return INSTANCE; } protected Object readResolve() { return INSTANCE; } }

Gerçek hayattan bir durum

Bir DatabaseConfig singleton'ının bağlantı havuzu parametrelerini ve kimlik bilgilerini yönettiği dağınık bir mikro hizmet mimarisini düşünelim. Bu hizmet, dağıtılan önbelleğe konfigürasyonu seri hale getirerek, dağıtım sonrası soğuk başlangıçları hızlandırır. Yatay ölçeklenme olayları sırasında, yeni hizmet örnekleri bu ikili bulutu alır ve seri hale getirir, istemeden varsayılan seri hale getirme protokolünü tetikler.

Savunma önlemleri olmadan, ObjectInputStream JVM'de tutulan statik INSTANCE'dan ayrı bir DatabaseConfig nesnesi oluşturur. Bu çoğaltma, yeni örneğin statik yapılandırma sırasında gerçekleştirilen başlatma kancalarını içermediği bir split-brain senaryosunu yaratır, bu da muhtemelen eski veritabanı uç noktalarına veya başlatılmamış kimlik bilgisi sağlayıcılarına işaret eder. Uygulama, birden fazla bağlantı havuzu yaratıldığı için kaynak sızıntılarından muzdarip olur, bu da veritabanı bağlantı sınırlarını zorlar ve kümeler arası zincirleme hatalara neden olur.

Bir yaklaşım, singleton'ı Enum türüne dönüştürmek olup, enum'ların spesifikasyona göre singletons olduğunu ve tasarım gereği seri hale getirmeye karşı dayanıklı olduğundan yararlanır. Artıları: Serileştirme mekanizması enum sabitlerini isim aramasıyla otomatik olarak işler, böylece örnek yaratımı tamamen engellenir. Eksileri: Enums, soyut sınıfları genişletemez, bu da mimari esnekliği kısıtlar ve tembel başlatma mantığına sahip değildir, bu da ağır konfigürasyonun sınıf başlatma sırasında gereksiz yere yüklenmesine yol açabilir.

Alternatif olarak, mevcut sınıfta readResolve yöntemini uygulamak, serileştirme tamamlandıktan sonra kanonik INSTANCE'ı döndürmesine olanak tanır. Artıları: Bu, miras hiyerarşilerini korur ve karmaşık başlatma mantığını desteklerken tekrar oluşturulan örneklerin oluşturulmasını önler. Eksileri: Geliştiriciler bu yöntemi sıklıkla gözden kaçırır ve singleton örneklemesi kendisi tembel başlatma ile başlatıldığında dikkatli senkronizasyon gerektirir; statik başlatma sırasında güvenlik garantisi sağlanmamış olabilir.

Üçüncü bir seçenek, Externalizable'ye geçmek olup, yalnızca yapılandırma tanımlarını yazmak için seri hale getirme akışını writeExternal ve readExternal aracılığıyla manuel olarak kontrol etmek gerekir. Artıları: Bu, nesne içeriğini seri hale getirmeyi reddederek instance yaratım saldırılarını engeller, bunun yerine readExternal sırasında güvenli bir depodan yapılandırmayı alır. Eksileri: Bu, önemli düzeyde şablon kodu ekler ve uygulama sürümleri arasında akış biçimlerinin geriye dönük uyumluluğunu sürdürmeyi gerektirir, bakım yükünü artırır.

Mühendislik ekibi, DatabaseConfig'nin, paylaşılan denetim kaydı işlevselliği için soyut bir BaseConfiguration sınıfını genişletmesi gerektiğinden, readResolve'ı uygulayarak statik INSTANCE'ı döndürmeyi seçti; bu nedenle enum'lar uygun değildi. Bu yaklaşımı, seri hale getirme sırasında senkronizasyon endişelerini önlemek için hızlı başlatma ile birleştirerek, singleton'ın herhangi bir seri hale getirmeden önce mevcut olmasını sağladılar. Bu yaklaşım, minimum kod müdahalesi ile sağlam bir koruma arasında bir denge sağladı.

Uygulama sonrası, yük testleri, önbelleğe alınmış konfigürasyonların serileştirilmesinin aynı nesne referanslarını döndürdüğünü doğruladı ve birden fazla bağlantı havuzunu ortadan kaldırdı. Hizmet, veritabanı bağlantısı tükenmesi olmadan yatay olarak ölçeklendi ve bellek profilleme, çöp toplama döngülerinden sonra yığın üzerinde ek DatabaseConfig örneklerinin kalmadığını doğruladı. Bu çözüm, mimari genişletilebilirliği korurken, serileştirme saldırılarına karşı singleton sözleşmesini güçlendirdi.

Adayların genellikle ıskalamış olduğu şeyler

readObject ve readResolve'nin etkileşimi, serileştirilmiş singleton'lardaki geçici alan durumunu nasıl etkiler?

readObject, akıştan tam nesne durumunu yeniden oluşturur, geçici alanlar için özel başlatma mantığını çalıştırır, JVM nesneyi tamamlanmış olarak değerlendirmeden önce. Ardından readResolve çalışır ve eğer farklı bir kanonik örnek döndürürse, JVM tamamen yeniden oluşturulmuş geçici nesneyi, geçici değerler dahil olmak üzere, yok sayar. Geliştiricilerin, geçici durumu kanonik örneğe almak için readResolve içinde manuel olarak kopyalamaları gerekir; ancak gerçek singleton'lar için geçici alanlar genel olarak kanonik durumdan yeniden türetilmelidir, seri hale getirilmiş akışlardan ziyade.

Externalizable uygulamak, readResolve'nin sağladığı korumaları neden aşar?

Externalizable arayüzü, seri hale getirme kontrolünü tamamen sınıfa kaydırır; bu da writeExternal ve readExternal aracılığıyla, varsayılan ObjectInputStream defaultReadObject mekanizmasını atlayarak, readResolve kontrolünü tamamen devre dışı bırakır. readExternal yeni oluşturulan bir örneği doldurduğunda, akış bunu son nesne olarak ele alır ve doğrudan döner, readResolve'yi çağırmak dışında, eğer geliştirici bunu readExternal içinde açıkça çağırmazsa. Bu mimari fark, Externalizable kullanan geliştiricilerin readExternal içinde örnek kontrol mantığını manuel olarak uygulamaları gerektiği anlamına gelir; bu genellikle InvalidObjectException fırlatmak veya durumu açıkça singleton'a birleştirmek yoluyla gerçekleştirilir, otomatik yerine kancayı kullanmak yerine.

Java Record türleri içinde readResolve'nin doğru çalışmasını ne engeller?

Kayıtlar, geleneksel sınıflar için kullanılan yansıma tabanlı alan populasyonunun aksine, ikincil kurucuları ve bileşen erişim yöntemleri aracılığıyla serileştirilir ve serileştirilir, yani serileştirme işlemi, readResolve'nin değiştirebileceği boş bir kabuk nesnesi oluşturmaz. JVM, kayıtları serileştirilmiş bileşen değerleri ile kurucu yöntemi çağırarak yeniden inşa eder; bu da readResolve'nin geçerli olmadığı anlamına gelir çünkü örnek tam olarak oluşturulur ve hemen oluşturulma sırasında değiştirilemez hale gelir. Kayıtlarla singleton benzeri bir davranış elde etmek için, geliştiriciler ya @Serial ile işaretlenmiş özel serileştirme proxy'leri için statik fabrika yöntemleri kullanmalıdır ya da readResolve aracılığıyla katı örnek kontrolü gerektiğinde standart sınıfları tercih etmelidir.