克隆、序列化、反射——单例模式防御心得

Java基础

浏览数:226

2019-2-23

AD:资源代下载服务

   单例模式有很多种写法,网上看到最多的就是下面这种:

   痴汉式的单例。

public class SingleTon {

    private static volatile SingleTon singleTon;

    private SingleTon() {
    }

    public static SingleTon getInstance(){
        if (singleTon == null){
            synchronized (SingleTon.class){
                if (singleTon == null){
singleTon = new SingleTon();
                }
            }
        }
        return singleTon;
    }
}

   然而很遗憾,这样并不能真正地实现单例,我有可能通过克隆、序列化、反射机制,来击破单例的模式。

   克隆,当你的单例类需要继承Cloneable接口时,就可以通过clone方法获取一个新的对象,那么单例防御失败。

   同理,序列化也可以,或者用反射,也可以。

  比如:

//获取构造函数
Constructor constructor = SingleTon.class.getDeclaredConstructor();
constructor.setAccessible(true);

   这样获取到了构造函数,设置可以访问,然后直接newInstance,就可以获取一个新的实例了。

  真正的单例,应当是可以抵御上述攻击的——

一,抵御clone攻击:

  首先给测试的类实现Cloneable接口,重写克隆方法:

/**
 * 防止克隆攻击
 * @return
* @throws CloneNotSupportedException
 */
@Override
protected Object clone() throws CloneNotSupportedException {
    return getInstance();
}

   So Easy

   测试代码:

SingleTon singleTon1 = getInstance();
SingleTon singleTon2 = (SingleTon) singleTon1.clone();
System.out.println(singleTon1 == singleTon2);//true

  输出true,通过

二,抵御序列化攻击

   给测试类实现Serializable接口,然后重写一个方法:

/**
 * 防止序列化攻击
 * @return
*/
private Object readResolve() {
    return getInstance();
}

   So Easy,比攻击的代码简单多了:

System.out.println("序列化攻击通过了吗?");
File file = new File("serializable.txt");
//序列化
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleTon1);
oos.flush();
oos.close();
fos.close();
//反序列化
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
SingleTon singleTon3 = (SingleTon) ois.readObject();
ois.close();
fis.close();
System.out.println(singleTon1 == singleTon3); // true

三,抵御反射

  这里的思路就是加一个flag,判断构造函数是否第一次被调用。

private static boolean flag = true;

private SingleTon() {
    if (flag){
flag = false;
        //code
    }else {
        throw new RuntimeException("对象已存在");
    }
}

   注意,这里的flag必须是private的,而且不能有getter setter函数。

  反射攻击的思路是,获取构造函数,然后获取到flag这个域,之后把这个flag置true,然后用构造函数新建。这里要求flag必须有getter setter,不然无法执行,这就是我们防御的思路。

  代码如下:

System.out.println("反射攻击通过了吗");
//获取构造函数
Constructor constructor = SingleTon.class.getDeclaredConstructor();
constructor.setAccessible(true);

//要求该域必须有getter,setter,否则java.beans.IntrospectionException: Method not found: isFlag
PropertyDescriptor descriptor = new PropertyDescriptor("flag", SingleTon.class);

//每次新建一个实例前,将flag设置为true
descriptor.getWriteMethod().invoke(SingleTon.class, true);
SingleTon singleTon4 = (SingleTon) constructor.newInstance();
System.out.println(singleTon1 == singleTon4);

   假如flag这个域有getter setter,则不会抛异常,最后会输出false。

   至此,我们完成的单例模式的代码如下:

public class SingleTon implements Serializable, Cloneable{

    private static final long serialVersionUID = 1L;

    private static volatile SingleTon singleTon;

    private static boolean flag = true;

    private SingleTon() {
        if (flag){
flag = false;
            //code
        }else {
            throw new RuntimeException("对象已存在");
        }
    }

    public static SingleTon getInstance(){
        if (singleTon == null){
            synchronized (SingleTon.class){
                if (singleTon == null){
singleTon = new SingleTon();
                }
            }
        }
        return singleTon;
    }

/**
 * 防止克隆攻击
 * @return
* @throws CloneNotSupportedException
 */
@Override
    protected Object clone() throws CloneNotSupportedException {
        return getInstance();
    }

// public static boolean getFlag() {
// return flag;
// }
//
// public static void setFlag(boolean flag) {
// SingleTon.flag = flag;
// }

/**
 * 防止序列化攻击
 * @return
*/
private Object readResolve() {
        return getInstance();
    }
}

   真是麻烦啊,一个什么都没有的类,为了单例,要写这么多东西。

   那么又没有捷径呢?

   当然有啦——

枚举

public enum SingleEnum implements Cloneable, Serializable{
INSTANCE;

    private String name;

    public SingleEnum getInstance(){
        System.out.println(this == INSTANCE); // true
        return INSTANCE;
    }

    public static void main(String[] args) {
        SingleEnum singleEnum = SingleEnum.INSTANCE;
        singleEnum.name = "枚举";
        System.out.println(singleEnum.name); // 枚举
        System.out.println(singleEnum.getInstance());  // true INSTANCE
    }
}

   一个枚举,就算实现双接口,也是无论如何都无法被破坏的。

  攻击代码如下:

System.out.println("枚举实验");
SingleEnum singleEnum1 = SingleEnum.INSTANCE;

System.out.println("直接获取");
SingleEnum singleEnum2 = SingleEnum.INSTANCE;
System.out.println(singleEnum1 == singleEnum2); // true

System.out.println("枚举克隆攻击通过了吗?");
System.out.println("枚举无法克隆");

System.out.println("枚举序列化攻击通过了吗?");
File enumTxt = new File("enumTest.txt");
//序列化
FileOutputStream fosEnum = new FileOutputStream(enumTxt);
ObjectOutputStream oosEnum = new ObjectOutputStream(fosEnum);
oosEnum.writeObject(singleEnum1);
oosEnum.flush();
oosEnum.close();
fosEnum.close();
//反序列化
FileInputStream fisEnum = new FileInputStream(enumTxt);
ObjectInputStream oisEnum = new ObjectInputStream(fisEnum);
SingleEnum singleEnum3 = (SingleEnum) oisEnum.readObject();
fisEnum.close();
oisEnum.close();
System.out.println(singleEnum1 == singleEnum3); // true

System.out.println("枚举反射攻击通过了吗?");
Class enumClass = singleEnum1.getClass();
/*
java.lang.InstantiationException
Caused by: java.lang.NoSuchMethodException: SingleEnum.<init>()
 */
//stop run
SingleEnum singleEnum5 = (SingleEnum) enumClass.newInstance();
System.out.println(singleEnum1 == singleEnum5);

//stop run
Constructor enumConstructor = SingleEnum.class.getConstructor(); // java.lang.NoSuchMethodException
enumConstructor.setAccessible(true);

SingleEnum singleEnum4 = (SingleEnum) enumConstructor.newInstance();
System.out.println(singleEnum1 == singleEnum4);

   直接获取:true

   克隆:枚举无法克隆,没有这样的方法。

   反射:没有构造函数,会抛出异常。就算你在枚举里加了构造函数,也是一样的。

   完美。

   希望对你有帮助。