java學習筆記-設計模式之單例模式如何防止反射及反序列化漏洞
阿新 • • 發佈:2019-01-04
在前一篇文章中,對單例模式列舉了五種實現方式。其中列舉模式擁有出生光環,天生就沒有反射及反序列化漏洞。針對其他四種實現方式,在本篇文章中對懶漢式單例模式實現進行反射及反序列化漏洞測試。
一、通過反射破解懶漢式單例模式
重新建立測試類TestClientNew,通過反射獲取到類,使用newInstance進行初始化。程式碼如下(詳細看註釋):
package com.zwh.gof23.singleton; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * 單例模式測試類,使用反射幾反序列化破解懶漢式單例模式 * * @author zwh * */ public class TestClientNew { /** * @param args * @throws ClassNotFoundException * @throws SecurityException * @throws NoSuchMethodException * @throws InvocationTargetException * @throws IllegalArgumentException * @throws IllegalAccessException * @throws InstantiationException */ public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { testSingletonPatternReflect(); } /** * 通過反射破解單例模式(懶漢式為例) * * @throws ClassNotFoundException * @throws NoSuchMethodException * @throws SecurityException * @throws InstantiationException * @throws IllegalAccessException * @throws IllegalArgumentException * @throws InvocationTargetException */ private static void testSingletonPatternReflect() throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { SingletonPatternSlackerNew s = SingletonPatternSlackerNew.getInstance(); System.out.println(s); // 通過反射得到懶漢式單例模式實現類 Class<SingletonPatternSlackerNew> clazz = (Class<SingletonPatternSlackerNew>) Class .forName("com.zwh.gof23.singleton.SingletonPatternSlackerNew"); // 獲取無參構造器 Constructor<SingletonPatternSlackerNew> c = clazz .getDeclaredConstructor(null); // 設定私有無參構造器可以訪問,跳過許可權檢查 c.setAccessible(true); // 初始化類的例項 SingletonPatternSlackerNew s1 = c.newInstance(); SingletonPatternSlackerNew s2 = c.newInstance(); System.out.println(s1); System.out.println(s2); } }
執行程式,輸出如下:
[email protected]
[email protected]
[email protected]
可得知,本此時方法得到了三個不同的例項。單例模式至此被破解。
二、預防反射破解單例模式
既然有破解的方式,那就可以防止被破解。通過在私有的構造器內進行例項判斷並丟擲異常,可以防止單例模式被利用反射破解。
私有構造器改造如下:
//私有化構造器 private SingletonPatternSlackerNew(){ //多次呼叫時,丟擲異常 if(instance!=null){ throw new RuntimeException("已存在例項,別想用反射來搞我!"); } }
改造後,再次執行測試方法。結果如下:
[email protected] Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.zwh.gof23.singleton.TestClientNew.testSingletonPatternReflect(TestClientNew.java:58) at com.zwh.gof23.singleton.TestClientNew.main(TestClientNew.java:28) Caused by: java.lang.RuntimeException: 已存在例項,別想用反射來搞我! at com.zwh.gof23.singleton.SingletonPatternSlackerNew.<init>(SingletonPatternSlackerNew.java:17) ... 6 more
此刻發現這樣的破解方式被有效防止了,但如果第一個例項,也是通過反射來建立的,這種方式就無法生效了。測試程式碼如下:
/**
* 通過反射破解單例模式(懶漢式為例)
*
* @throws ClassNotFoundException
* @throws NoSuchMethodException
* @throws SecurityException
* @throws InstantiationException
* @throws IllegalAccessException
* @throws IllegalArgumentException
* @throws InvocationTargetException
*/
private static void testSingletonPatternReflect()
throws ClassNotFoundException, NoSuchMethodException,
SecurityException, InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
/*SingletonPatternSlackerNew s = SingletonPatternSlackerNew.getInstance();
System.out.println(s);*/
// 通過反射得到懶漢式單例模式實現類
Class<SingletonPatternSlackerNew> clazz = (Class<SingletonPatternSlackerNew>) Class
.forName("com.zwh.gof23.singleton.SingletonPatternSlackerNew");
// 獲取無參構造器
Constructor<SingletonPatternSlackerNew> c = clazz
.getDeclaredConstructor(null);
// 設定私有無參構造器可以訪問,跳過許可權檢查
c.setAccessible(true);
// 初始化類的例項
SingletonPatternSlackerNew s1 = c.newInstance();
SingletonPatternSlackerNew s2 = c.newInstance();
System.out.println(s1);
System.out.println(s2);
}
在這次測試中將直接呼叫getInstance方法得到例項的程式碼註釋掉,兩個例項都通過反射來建立。輸出結果:
[email protected]
[email protected]
這說明構造器中的方法還無法避免被反射破解。對構造器進行修改。最終程式碼如下:
package com.zwh.gof23.singleton;
import java.io.Serializable;
/**
* 懶漢式到單例模式(防序列化及反射漏洞)
* @author zwh
* 特點:
* 1、延遲載入,在呼叫到getInstance方法時才載入。資源利用率高
* 2、呼叫getInstance方法時需要同步,併發效率較低
*/
public class SingletonPatternSlackerNew implements Serializable{
//例項不初始化,需要時再進行初始化
private static SingletonPatternSlackerNew instance;
private static int count = 0;
//私有化構造器
private SingletonPatternSlackerNew(){
synchronized (SingletonPatternSlackerNew.class) {
if(count > 0){
throw new RuntimeException("建立了兩個例項");
}
count++;
}
//多次呼叫時,丟擲異常
if(instance!=null){
throw new RuntimeException("已存在例項,別想用反射來搞我!");
}
}
/**
* 初始化類的例項,並保證在呼叫本方法時才會建立例項,保證執行緒安全。
* @return
*/
public static synchronized SingletonPatternSlackerNew getInstance(){
if(null == instance){
instance = new SingletonPatternSlackerNew();
}
return instance;
}
/**
* 防止被反序列化破解單例
* @return
*/
private Object readResolve(){
return instance;
}
}
再次執行測試程式碼,結果如下:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.zwh.gof23.singleton.TestClientNew.testSingletonPatternReflect(TestClientNew.java:90)
at com.zwh.gof23.singleton.TestClientNew.main(TestClientNew.java:36)
Caused by: java.lang.RuntimeException: 建立了兩個例項
at com.zwh.gof23.singleton.SingletonPatternSlackerNew.<init>(SingletonPatternSlackerNew.java:21)
... 6 more
避免了單例被反射多次建立。
注:構造器內的判斷instance為空程式碼可以去掉。
到此,防止反射破解單例模式完成。
三、通過反序列化破解懶漢式單例模式
除了前面描述的反射來破解單例模式,通過反序列化,也可以破解單例模式。當然前提是,實現單例模式的類需要可以序列化。通過實現Serializable介面即可。
測試方法如下:
/**
* 通過反序列化破解單例模式漏洞,前提是單例類可序列化。
* @throws IOException
* @throws ClassNotFoundException
*/
private static void testSingletonPatternSerialize() throws IOException, ClassNotFoundException{
SingletonPatternSlackerNew s=SingletonPatternSlackerNew.getInstance();
System.out.println(s);
FileOutputStream fos=new FileOutputStream("d:/a.txt");
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(s);
oos.close();
fos.close();
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("d:/a.txt"));
SingletonPatternSlackerNew s1=(SingletonPatternSlackerNew) ois.readObject();
System.out.println(s1);
}
執行結果:
[email protected]
[email protected]
可見,產生了兩個不同的例項。
四、防止反序列化破解單例
要防止被反序列化破解單例。一種不準單例類被序列化。另一種是在單例類中宣告readResolve方法,返回例項。程式碼如下:
/**
* 防止被反序列化破解單例
* @return
*/
private Object readResolve(){
return instance;
}
再次執行輸出結果:
[email protected]
[email protected]
兩次得到了同一個例項。
至此,通過反射幾反序列化破解單例模式,以及如何防止單例模式被反射和反序列化的破解學習完畢。