第3條講述了 Singleton 模式,并且給出了以下這個 Singleton 類的示例。
成都創(chuàng)新互聯(lián)專注于企業(yè)全網營銷推廣、網站重做改版、蘭山網站定制設計、自適應品牌網站建設、H5開發(fā)、商城系統(tǒng)網站開發(fā)、集團公司官網建設、成都外貿網站建設、高端網站制作、響應式網頁設計等建站業(yè)務,價格優(yōu)惠性價比高,為蘭山等各大城市提供網站開發(fā)制作服務。這個類限制了對其構造器的訪問,確保永遠只創(chuàng)建一個實例:
public class Singer{
public static final Singer INSTANCE = new Singer();
private Singer(){}
public static Singer getInstance(){
return INSTANCE;
}
}
但如果在這個類實現(xiàn)序列化,這種方式就不能保證安全了。序列化可以輕松突破這種機制,甚至,這種錯誤是在無意間的,并不是蓄意破壞。之前曾說過,序列化是一種獨立于構造器的創(chuàng)建對象的機制,或者你可以變相地認為序列化是一種以 byte[] 為參數(shù)的隱形構造器。
演示:
public class Singer implements Serializable {
public static final Singer INSTANCE = new Singer();
private Singer(){}
public static Singer getInstance(){
return INSTANCE;
}
private Object readResolve(){
return INSTANCE;
}
}
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singer instance = Singer.getInstance();
//序列化
File file = new File("C:\\Users\\admin\\Desktop\\file_upload\\1.txt");
ser(instance,file);
//反序列化
byte[] bytes = readBytes(file);
Singer instance2 = deSer(bytes);
//比較兩個對象是否相同
System.out.println(instance == instance2);
}
static void ser(Singer s,File file) throws IOException {
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(s);
os.close();
}
static Singer deSer(byte[] bytes) throws IOException, ClassNotFoundException {
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
Singer p = (Singer) in.readObject();
in.close();
return p;
}
static byte[] readBytes(File file) throws IOException {
FileInputStream in = null;
try {
in =new FileInputStream(file);
//當文件沒有結束時,每次讀取一個字節(jié)顯示
byte[] data=new byte[in.available()];
in.read(data);
in.close();
return data;
} catch (IOException e) {
e.printStackTrace();
}finally {
in.close(); //關閉流
}
return null;
}
}
false
可以看出,反序列化的對象和單例獲得的對象并不相同,從而導致單例模式失效
readResole 特性允許你用 readObject 創(chuàng)建的實例代替另一個實例。對于一個正在被反序列化的對象,如果它的類定義了一個 readResolve 方法,并且具備正確的聲明,那么在反序列化之后,新建對象上的 readResolve 方法就會被調用。然后,該方法返回的對象應用將被返回,取代新建的對象。在這個特性的絕大多數(shù)用法中,指向新建對象的引用不需要再被保留,因此立即成為垃圾回收的對象。
比如,使用以下的方法保證單例
private Object readResolve(){
return INSTANCE;
}
該方法忽略了被反序列化的對象,只返回該類初始化時創(chuàng)建的那個特殊的實例。因此,Singer 實例的序列化形式并不需要包含任何實際的數(shù)據;所有的實例域都應該聲明為 transient。
事實上,如果依賴 readResolve 進行實例控制,帶有對象引用類型的所有實例域則都必須聲明為 transient。否則,攻擊者依然可以想辦法在 readResolve 運行之前,獲取反序列化的對象引用,得到一個單例之外的“副本”,類似于上一條中提到的 MutablePeriod攻擊。
這種攻擊有點復雜,但背后的思想卻很簡單。如果 Singleton 包含一個非 transient 對象引用域,這個域的內容就可以在 Singleton 的 readResolve 方法運行之前被反序列化。當對象引用域的內容被反序列化時,它就允許一個精心制作的流“盜用”指向最初被反序列化的 Singleton 引用。
實例演示:
import java.io.Serializable;
import java.util.Arrays;
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis(){}
private String[] favoriteSongs = {"紅顏如霜","發(fā)如雪"};
public void printFavorites(){
System.out.println(Arrays.toString(favoriteSongs));
}
private Object readResolve(){
return INSTANCE;
}
}
import java.io.Serializable;
public class ElvisStealer implements Serializable {
static Elvis impersonator;
private Elvis payload;
private Object readResolve(){
impersonator = payload;
return new String[]{"發(fā)如霜"};
}
private static final long serialVersionUID = 0;
}
import lombok.SneakyThrows;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
public class ElvisImpersonator {
private static final byte[] bytes = new byte[]{
(byte) 0xac, (byte) 0xed,0x00,0x05,0x73,0x72,0x00,0x05,
0x45,0x6c,0x76,0x69,0x73, (byte) 0x84, (byte) 0xe6,
(byte) 0x93,0x33, (byte) 0xc3, (byte) 0xf4, (byte) 0x8b,
0x32,0x02,0x00,0x01,0x4c,0x00,0x0d,0x66,0x61,0x76,
0x6f,0x72,0x69,0x74,0x65,0x53,0x6f,0x6e,0x67,0x73,
0x74,0x00,0x12,0x4c,0x6a,0x61,0x76,0x61,0x2f,0x6c,
0x61,0x6e,0x67,0x2f,0x4f,0x62,0x6a,0x65,0x63,0x74,
0x3b,0x78,0x70,0x73,0x72,0x00,0x0c,0x45,0x6c,0x76,
0x69,0x73,0x53,0x74,0x65,0x61,0x6c,0x65,0x72,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x01,
0x4c,0x00,0x07,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64,
0x74,0x00,0x07,0x4c,0x45,0x6c,0x76,0x69,0x73,0x3b,
0x78,0x70,0x71,0x00,0x7e,0x00,0x02
};
public static void main(String[] args) {
Elvis elvis = deser(bytes);
Elvis impersonator = ElvisStealer.impersonator;
elvis.printFavorites();
impersonator.printFavorites();
}
@SneakyThrows
static Elvis deser(byte[] bytes) {
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
Elvis p = (Elvis) in.readObject();
in.close();
return p;
}
}
通過將 favorites 聲明為 transient 可以解決這個問題。但如果確實要避免出現(xiàn)這種錯誤,最好的還是使用 枚舉來解決這種問題。
正如之前所提到過的:自從 jdk1.5之后,單例的最佳實現(xiàn)方式就是枚舉。當然,readResolve 并非完全過時,有些情況可能不適合使用枚舉,這時候依然需要這種方案。但一定要注意,屬性設置為transient 或者 基本類型
你是否還在尋找穩(wěn)定的海外服務器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調度確保服務器高可用性,企業(yè)級服務器適合批量采購,新人活動首月15元起,快前往官網查看詳情吧