1. 程式人生 > >java學習筆記-設計模式之單例模式如何防止反射及反序列化漏洞

java學習筆記-設計模式之單例模式如何防止反射及反序列化漏洞

在前一篇文章中,對單例模式列舉了五種實現方式。其中列舉模式擁有出生光環,天生就沒有反射及反序列化漏洞。針對其他四種實現方式,在本篇文章中對懶漢式單例模式實現進行反射及反序列化漏洞測試。

一、通過反射破解懶漢式單例模式

重新建立測試類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]

兩次得到了同一個例項。

至此,通過反射幾反序列化破解單例模式,以及如何防止單例模式被反射和反序列化的破解學習完畢。