java设计模式之单例设计模式——-反射攻击解决方案及原理分析
一、仍然以实例引入,发现问题、解决问题的思路进行。
1)问题:鉴于反射可以任意创建对象的特点,猜测,是否存在通过反射在外部通过调用私有构造方法创建对象,打破单例模式特点的可能?
2)仍然以饿汉模式的例子进行示例(当然也可通过其他方式)。
代码如下,先运行,看不破坏的情况下是什么样
package com.zxl.design.zxl.design.pattern.singleton;
import java.io.ObjectInputStream;
import java.io.Serializable;
/**
* Created by Administrator on 2019/6/23.
*/
public class HungrySingleton implements Serializable{
//写法简单的单例模式
//类加载时就初始化,没有多线程问题,但是可能会造成资源浪费,
//比如该对象没有被使用
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getHungrySingleton(){
return hungrySingleton;
}
private Object readResolve(){
return hungrySingleton;
}
}
package com.zxl.design.zxl.design.pattern.singleton;
import java.net.SocketPermission;
/**
* Created by Administrator on 2019/6/23.
*/
public class T implements Runnable {
public void run() {
HungrySingleton hungrySingleton = HungrySingleton.getHungrySingleton();
System.out.println(Thread.currentThread().getName() +" "+ hungrySingleton);
}
}
package com.zxl.design.zxl.design.pattern.singleton;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Created by Administrator on 2019/6/16.
*/
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
//如上可能会爆红,直接简单抛出异常(注意我们这里简单起见,真实业务场景要具体分析)
//通过正常单例模式构建对象的方式进行创建
HungrySingleton hungrySingleton =HungrySingleton.getHungrySingleton();
//通过反射构造器类创建对象的方式进行创建
HungrySingleton newHungrySingleInstance = (HungrySingleton) constructor.newInstance();
System.out.println(hungrySingleton);
System.out.println(newHungrySingleInstance);
System.out.println(hungrySingleton == newHungrySingleInstance);
}
}
然后运行,查看结果,具体如下:
Exception in thread "main" java.lang.IllegalAccessException: Class com.zxl.design.zxl.design.pattern.singleton.Test can not access a member of class com.zxl.design.zxl.design.pattern.singleton.HungrySingleton with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
at java.lang.reflect.Constructor.newInstance(Constructor.java:413)
at com.zxl.design.zxl.design.pattern.singleton.Test.main(Test.java:40)
Process finished with exit code 1
如上所述,这里有权限不合格异常。
解析:因为私有方法在外界不能随意调用,但根据提示,是否存在一种方法可以给予相应的权限,使得满足条件,而进行使用呢?
答案当然是有的。反射中有可以赋予权限的操作,contructor 类有个setAccessible()方法,我们尝试一下。
我们在测试类中,为constructor放开权限,增加如下一句代码constructor.setAccessible(true);
具体如下
package com.zxl.design.zxl.design.pattern.singleton;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Created by Administrator on 2019/6/16.
*/
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
//增加如下方法,看能否正常运行
constructor.setAccessible(true);
//如上可能会爆红,直接简单抛出异常(注意我们这里简单起见,真实业务场景要具体分析)
//通过正常单例模式构建对象的方式进行创建
HungrySingleton hungrySingleton =HungrySingleton.getHungrySingleton();
//通过反射构造器类创建对象的方式进行创建
HungrySingleton newHungrySingleInstance = (HungrySingleton) constructor.newInstance();
System.out.println(hungrySingleton);
System.out.println(newHungrySingleInstance);
System.out.println(hungrySingleton == newHungrySingleInstance);
}
}
运行后,看到测试结果如下
Connected to the target VM, address: '127.0.0.1:62762', transport: 'socket'
[email protected]e721
[email protected]2c14
false
Disconnected from the target VM, address: '127.0.0.1:62762', transport: 'socket'
意外出现,通过反射,我们竟然能在单例类外面通过放开构造器权限的方式创建了一个对象,这必然是违背单例设计模式初衷的,必然是不安全的,万一有黑客想攻击,岂不是so easy?
那怎么防御呢?????
三、防范方案逐步升级。
1、针对类加载时就初始化的单例设计模式,包括饿汉式和静态内部类的方式,可以采用在构造器中,添加实例是否为空的方式进行判断。
如果不为空,就抛出运行时异常,这样就能防御该类型攻击,具体操作代码如下:
package com.zxl.design.zxl.design.pattern.singleton;
import java.io.ObjectInputStream;
import java.io.Serializable;
/**
* Created by Administrator on 2019/6/23.
*/
public class HungrySingleton implements Serializable{
//写法简单的单例模式
//类加载时就初始化,没有多线程问题,但是可能会造成资源浪费,
//比如该对象没有被使用
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
//为构造器添加反射调用判断
if (hungrySingleton != null){
throw new RuntimeException("单例设计模式构造器禁止反射调用");
}
}
public static HungrySingleton getHungrySingleton(){
return hungrySingleton;
}
private Object readResolve(){
return hungrySingleton;
}
}
其他内容不变,再次运行:
"D:\Program Files\Java\jdk1.8.0_102\bin\java" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:62966,suspend=y,server=n -Dfile.encoding=UTF-8 -classpath "D:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;C:\Users\Administrator\Desktop\gson-master\gson-master\DesignMode\target\classes;D:\InteliijIDea\IntelliJ IDEA 2017.1.4\lib\idea_rt.jar" com.zxl.design.zxl.design.pattern.singleton.Test
Connected to the target VM, address: '127.0.0.1:62966', transport: 'socket'
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)
Disconnected from the target VM, address: '127.0.0.1:62966', transport: 'socket'
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.zxl.design.zxl.design.pattern.singleton.Test.main(Test.java:42)
Caused by: java.lang.RuntimeException: 单例设计模式构造器禁止反射调用
at com.zxl.design.zxl.design.pattern.singleton.HungrySingleton.<init>(HungrySingleton.java:21)
... 5 more
Process finished with exit code 1
如上可得,该方案生效了,那测试通过静态内部类的方式创建的单例是否也可采用该方案。
先通过不做处理的静态内部类,来确认下:
package com.zxl.design.zxl.design.pattern.singleton;
/**
* Created by Administrator on 2019/6/23.
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton( ){
//注意该方法不能少,否则会从外面创建一个对象出来。
}
private static class InnerClass{
//该方案重点就在于如下私有类的初始化锁,重点看谁先获取到该初始化锁,
//一旦获取到该初始化锁,会立刻执行初始化,并且该初始化过程对于其他
//线程是不可见的。
private static StaticInnerClassSingleton innerClassSingleton=
new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getStaticInnerClassInstance(){
return InnerClass.innerClassSingleton;
}
}
package com.zxl.design.zxl.design.pattern.singleton;
import java.net.SocketPermission;
/**
* Created by Administrator on 2019/6/23.
*/
public class T implements Runnable {
public void run() {
// LazyDoubleCheckSingleton lazySingleton =
// LazyDoubleCheckSingleton.getDoubleSingletonInstance();
StaticInnerClassSingleton staticInnerClassSingleton =
StaticInnerClassSingleton.getStaticInnerClassInstance();
//HungrySingleton hungrySingleton = HungrySingleton.getHungrySingleton();
System.out.println(Thread.currentThread().getName() +" "+ staticInnerClassSingleton);
}
}
package com.zxl.design.zxl.design.pattern.singleton;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Created by Administrator on 2019/6/16.
*/
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//Class objectClass = HungrySingleton.class;
Class objectClass = StaticInnerClassSingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
//增加如下方法,看能否正常运行
// constructor.setAccessible(true);
//如上可能会爆红,直接简单抛出异常(注意我们这里简单起见,真实业务场景要具体分析)
//通过正常单例模式构建对象的方式进行创建
// HungrySingleton hungrySingleton =HungrySingleton.getHungrySingleton();
StaticInnerClassSingleton innerClassSingleton =StaticInnerClassSingleton.getStaticInnerClassInstance();
//通过反射构造器类创建对象的方式进行创建
// HungrySingleton newHungrySingleInstance = (HungrySingleton) constructor.newInstance();
StaticInnerClassSingleton newStaticInnerSingleton = (StaticInnerClassSingleton) constructor.newInstance();
System.out.println(innerClassSingleton);
System.out.println(newStaticInnerSingleton);
System.out.println(innerClassSingleton == newStaticInnerSingleton);
}
}
运行结果如下
"D:\Program Files\Java\jdk1.8.0_102\bin\java" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:63056,suspend=y,server=n -Dfile.encoding=UTF-8 -classpath "D:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;C:\Users\Administrator\Desktop\gson-master\gson-master\DesignMode\target\classes;D:\InteliijIDea\IntelliJ IDEA 2017.1.4\lib\idea_rt.jar" com.zxl.design.zxl.design.pattern.singleton.Test
Connected to the target VM, address: '127.0.0.1:63056', transport: 'socket'
Exception in thread "main" java.lang.IllegalAccessException: Class com.zxl.design.zxl.design.pattern.singleton.Test can not access a member of class com.zxl.design.zxl.design.pattern.singleton.StaticInnerClassSingleton with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
at java.lang.reflect.Constructor.newInstance(Constructor.java:413)
at com.zxl.design.zxl.design.pattern.singleton.Test.main(Test.java:45)
Disconnected from the target VM, address: '127.0.0.1:63056', transport: 'socket'
Process finished with exit code 1
可以看到,这个和饿汉式之前的情形一致,我们在此添加上打开权限的方式
也就是将如上测试代码中 constructor.setAccessible(true); 注释掉的放开,运行结果如下
"D:\Program Files\Java\jdk1.8.0_102\bin\java" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:63076,suspend=y,server=n -Dfile.encoding=UTF-8 -classpath "D:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;C:\Users\Administrator\Desktop\gson-master\gson-master\DesignMode\target\classes;D:\InteliijIDea\IntelliJ IDEA 2017.1.4\lib\idea_rt.jar" com.zxl.design.zxl.design.pattern.singleton.Test
Connected to the target VM, address: '127.0.0.1:63076', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:63076', transport: 'socket'
com.zx[email protected]69222c14
com.zx[email protected]606d8acf
false
Process finished with exit code 0
进一步,我们在静态内部类的私有构造器方法中添加为空判断,不为空则抛出运行时异常。
具体如下:
package com.zxl.design.zxl.design.pattern.singleton;
/**
* Created by Administrator on 2019/6/23.
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton( ){
//注意该方法不能少,否则会从外面创建一个对象出来。
if (getStaticInnerClassInstance() != null){
throw new RuntimeException("单例设计模式禁止反射调用私有构造器");
}
}
private static class InnerClass{
//该方案重点就在于如下私有类的初始化锁,重点看谁先获取到该初始化锁,
//一旦获取到该初始化锁,会立刻执行初始化,并且该初始化过程对于其他
//线程是不可见的。
private static StaticInnerClassSingleton innerClassSingleton=
new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getStaticInnerClassInstance(){
return InnerClass.innerClassSingleton;
}
}
如上更改,再次运行。
Connected to the target VM, address: '127.0.0.1:63115', transport: 'socket'
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.zxl.design.zxl.design.pattern.singleton.Test.main(Test.java:45)
Caused by: java.lang.RuntimeException: 单例设计模式禁止反射调用私有构造器
at com.zxl.design.zxl.design.pattern.singleton.StaticInnerClassSingleton.<init>(StaticInnerClassSingleton.java:11)
... 5 more
Disconnected from the target VM, address: '127.0.0.1:63115', transport: 'socket'
Process finished with exit code 1
如上,我们成功做到了抵御反射攻击。
通过以上测试,我们可以发现,对于类加载时就初始化的单例设计模式是可行的,那换成懒加载的方式,是否也可用呢?我们可以先测试下
上一篇: linux非阻塞IO与阻塞IO的应用
下一篇: 信号的介绍及使用