Singleton单例模式-如何防止序列化对单例类的攻击?

既然是防止序列化的攻击,简单的做法就是这个单例类不要实现Serializable接口即可(是不是比较简单,嘻嘻),不过呢可能有面试官会问到如果实现了Serializable接口那如何防止破坏单例模式呢?

先来一个“懒汉式-防止指令重排优化的懒汉式”示例,参见
https://blog.csdn.net/hl_java/article/details/70148622 ,本文这个示例在原来的基础上增加了抗反射攻击的

public class MyManger7 implements Serializable {
    //这里两行的顺序非常关键,当前行(public static boolean flag = false;)一定要在(private static MyManger7 instance;)的前面
    //因为下面这一行new MyManger7()其中的构造器会修改flag的值,如果顺序被调整,new MyManger7()中修改过的值后来
    //又会被static boolean flag = false;覆盖,从而没有目的
    public static boolean flag = false;
    private static volatile MyManger7 instance;

    private MyManger7() {
        synchronized (MyManger7.class) {
            if (flag == false) {
                flag = true;
            } else {
                throw new RuntimeException("正在遭受反射攻击");
            }
        }
    }

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

反序列化攻击测试用例:

public class ReflectSingletonTest {
    public static void main(String[] args) throws Exception {
        MyManger7 sc1 = MyManger7.getInstance();
        MyManger7 sc2 = MyManger7.getInstance();
        System.out.println("第1次getInstance得到的对象:" + sc1);
        System.out.println("第2次getInstance得到的对象:" + sc2);
        System.out.println("比较2次getInstance得到的对象是否相同:" + (sc1 == sc2)); // sc1,sc2是同一个对象

        /**
         * 通过反序列化的方式构造多个对象(单例类需要实现Serializable接口)
         */
        // 1. 把对象sc1写入硬盘文件
        FileOutputStream fos = new FileOutputStream("object.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(sc1);
        oos.close();
        fos.close();

        // 2. 把硬盘文件上的对象读出来
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        // 如果对象定义了readResolve()方法,readObject()会调用readResolve()方法。从而解决反序列化的漏洞
        MyManger7 sc5 = (MyManger7) ois.readObject();
        // 反序列化出来的对象,和原对象,不是同一个对象。如果对象定义了readResolve()方法,可以解决此问题。
        System.out.println("序列化出来的对象:" + sc5);
        ois.close();

        System.out.println("判断由对象sc1序列化后,又反序列化得到的是否是同一个对象:" + (sc1 == sc5));

    }
}

测试结果:

第1次getInstance得到的对象:com.alioo.format.service.test.single.MyManger7@41cf53f9
第2次getInstance得到的对象:com.alioo.format.service.test.single.MyManger7@41cf53f9
比较2次getInstance得到的对象是否相同:true
序列化出来的对象:com.alioo.format.service.test.single.MyManger7@61e717c2
判断由对象sc1序列化后,又反序列化得到的是否是同一个对象:false

根据上面最后一行日志,可以看到的确被攻击了,优化后的单例代码如下:

public class MyManger7 implements Serializable {
    //这里两行的顺序非常关键,当前行(public static boolean flag = false;)一定要在(private static MyManger7 instance;)的前面
    //因为下面这一行new MyManger7()其中的构造器会修改flag的值,如果顺序被调整,new MyManger7()中修改过的值后来
    //又会被static boolean flag = false;覆盖,从而没有目的
    public static boolean flag = false;
    private static volatile MyManger7 instance;

    private MyManger7() {
        synchronized (MyManger7.class) {
            if (flag == false) {
                flag = true;
            } else {
                throw new RuntimeException("正在遭受反射攻击");
            }
        }
    }

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

    // 防止反序列化获取多个对象的漏洞。
    // 无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
    // 实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。
    private Object readResolve() throws ObjectStreamException {
        return instance;
    }

}

再次使用反序列化攻击测试用例,运行一下验证结果:

第1次getInstance得到的对象:com.alioo.format.service.test.single.MyManger7@41cf53f9
第2次getInstance得到的对象:com.alioo.format.service.test.single.MyManger7@41cf53f9
比较2次getInstance得到的对象是否相同:true
序列化出来的对象:com.alioo.format.service.test.single.MyManger7@41cf53f9
判断由对象sc1序列化后,又反序列化得到的是否是同一个对象:true

可以看到反序列化之后得到的对象就是之前通过MyManger7.getInstance()得到的对象,所以这个时候的反序列化攻击是失败的。

题外话
既然是单例类,本身也提供了获取单例的方式方法public static MyManger7 getInstance(), 所以我们原则上不是不希望还有其它的获取单例的方式的。
也就是说我们是不希望通过反序列化来获取单例的,那我们就真的不需要让这个单例类实现Serializable接口的,自然而然也就可以避免反序列化的攻击(此为个人理解,不知对否,如有高手路过,肯请指点)

作者相关文章

Singleton单例模式的几种创建方法
Singleton单例模式-如何防止JAVA反射对单例类的攻击?
Singleton单例模式-如何防止序列化对单例类的攻击?
Singleton单例模式-【懒汉式-加双重校验锁&防止指令重排序的懒汉式】实现方案中为什么需要加volatile关键字?

发布了100 篇原创文章 · 获赞 64 · 访问量 25万+

猜你喜欢

转载自blog.csdn.net/hl_java/article/details/87464951