既然是防止序列化的攻击,简单的做法就是这个单例类不要实现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关键字?