前言:
在并发环境中,我们可以用各种锁来保持单例的线程安全,当然这是从业务角度来考虑的;但是,从一个攻击者的角度来看,你的单例也许只保证了线程安全,当攻击者通过反射new出单例的实例时候(反射的可以改变你的私有构造函数),那攻击者就可以控制你的单例干很多坏事
举例:
单例代码:
/**
* 懒汉式单例(简单点),不考虑多线程并发的模型
*/
public class MyInstance {
private static MyInstance instance;
private MyInstance(){
}
public static MyInstance getInstance(){
if (instance == null){
instance = new MyInstance();
}
return instance;
}
}
攻击代码:
import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args){
System.out.println(MyInstance.getInstance());
try
{
Class<MyInstance> classType = MyInstance.class;
Constructor<MyInstance> constructor = classType.getDeclaredConstructor(null);
constructor.setAccessible(true);//关键代码,将这个构造函数的权限设置为可进入的
MyInstance myInstanceHack = constructor.newInstance();
System.out.println(myInstanceHack);
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
攻击结果:
从结果来看,两个对象的地址是不一样的,那么攻击者就可以利用这个方式,干很多越权的事情。
如何防范呢?
其实,问题所在就是那个构造函数被攻击了,因此,我们只要保护好构造函数,就能基本防御这种攻击
方式一:
利用一个静态变量,保证构造函数只被掉一次
/**
* 懒汉式单例(简单点),不考虑多线程并发的模型
*/
public class MyInstance {
private static MyInstance instance;
private static boolean isUsed;
private MyInstance(){
if (isUsed){
//抛出异常
throw new RuntimeException();
}else {
isUsed = true;
}
}
public static MyInstance getInstance(){
if (instance == null){
instance = new MyInstance();
}
return instance;
}
}
方式二:
利用枚举的特性:每个枚举量只会实例化一次,也就是枚举类的单例:
public enum MyInstanceEnum {
INSTANCE;
private MyInstance2 myInstance;
MyInstanceEnum(){
myInstance = new MyInstance2();
}
public MyInstance2 getMyInstance() {
return myInstance;
}
}
攻击时报错
总结:
当然,反射被设计出来,肯定不是用来攻击的,它的作用可以很多依赖注入的框架中,比如Spring各种框架,JSON解析等很多方面。(不晓得从哪里看到一句话:人的血液循环是复杂但是有规律的,打针(反射)在中间介入这个循环如果没摸清楚循环规律而乱戳表面看似达到效果,实际会引发其他问题)