破解单例模式:反射、序列化与克隆攻击的防御之道

开发 前端
对 EnumSingleton 文件进行反编译,可以发现 EnumSingleton 继承于 Enum,而 Enum 类确实没有无参的构造器,所以抛出 NoSuchMethodException。

可能有人看了我上一篇文章里几种方式对比的表格,觉得枚举有缺点,为什么Joshua Bloch还推荐使用枚举?

这就要提到单例的破解了。普通的单例模式是可以通过反射和序列化/反序列化来破解的,而Enum由于自身的特性问题,是无法破解的。当然,由于这种情况基本不会出现,因此我们在使用单例模式的时候也比较少考虑这个问题。

枚举类是实现单例模式最好的方式

在单例模式的实现中,除去枚举方法实现的单例模式,其它的实现都可以利用反射构造新的对象,从而破坏单例模式,但是枚举就不行,下面说说原因:
破坏单例的方式有 3 种,反射、克隆以及序列化,下面详细介绍:

反射

常见的单例模式实现中,往往有一个私有的构造函数,防止外部程序的调用,但是通过反射可以轻而易举的破坏这个限制:

public class DobleCheckSingleton {

    private DobleCheckSingleton(){}

    private static volatile DobleCheckSingleton dobleCheckSingleton;

    public static DobleCheckSingleton getSingleton(){
        if (dobleCheckSingleton == null){
            synchronized (DobleCheckSingleton.class){
                if (dobleCheckSingleton == null){
                    dobleCheckSingleton = new DobleCheckSingleton();
                }
            }
        }
        return dobleCheckSingleton;
    }

    public static void main(String[] args) {
        try {
            DobleCheckSingleton dobleCheckSingleton = DobleCheckSingleton.getSingleton();
            Constructor<DobleCheckSingleton> constructor = DobleCheckSingleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            DobleCheckSingleton reflectInstance = constructor.newInstance();
            System.out.println(dobleCheckSingleton == reflectInstance);
        } catch (Exception e  e.printStackTrace();
        }
    }
}

输出:false,单例被破坏
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

显然,通过反射可以破坏所有含有无参构造器的单例类,如可以破坏懒汉式、饿汉式、静态内部类的单例模式。

但是反射无法破坏通过枚举实现的单例模式,利用反射构造新的对象,由于 enum 没有无参构造器,结果会抛出 NoSuchMethodException 异常。

public enum EnumSingleton {

    INSTANCE;

    public static void main(String[] args) {
        try {
            EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
            // 获取无参的构造函数
            Constructor<EnumSingleton> constructor = null;
            constructor = EnumSingleton.class.getDeclaredConstructor();
            // 使用构造函数创建对象
            constructor.setAccessible(true);
            EnumSingleton reflectInstance = constructor.newInstance();
            System.out.println(enumSingleton == reflectInstance);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:java.lang.NoSuchMethodException: singleton.EnumSingleton.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at singleton.EnumSingleton.main(EnumSingleton.java:19)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

枚举安全的原因解释:

对 EnumSingleton 文件进行反编译,可以发现 EnumSingleton 继承于 Enum,而 Enum 类确实没有无参的构造器,所以抛出 NoSuchMethodException。

枚举类 EnumSingleton 反编译结果
public final class singleton.EnumSingleton extends java.lang.Enum<singleton.EnumSingleton>

Enum 类的构造方法
protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

进一步,通过调用父类有参构造器构造枚举实例对象,样例程序又抛出 IllegalArgumentException 异常。

public enum EnumSingleton {

    INSTANCE;

    public EnumSingleton getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) {
        try {
            EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
            // 获取无参的构造函数
            Constructor<EnumSingleton> constructor = null;
            constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
            // 使用构造函数创建对象
            constructor.setAccessible(true);
            EnumSingleton reflectInstance = constructor.newInstance("test",1);
            System.out.println(enumSingleton == reflectInstance);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at singleton.EnumSingleton.main(EnumSingleton.java:31)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

因为 Constructor 的 newInstance 方法限定了 clazz 的类型不能是 enum,否则抛出异常。

@CallerSensitive
 public T newInstance(Object ... initargs)
     throws InstantiationException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException
 {
    ...
     if ((clazz.getModifiers() & Modifier.ENUM) != 0)
         throw new IllegalArgumentException("Cannot reflectively create enum objects");
     ...
 }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

所以枚举类不能通过反射构建构造函数的方式构建新的实例。

序列化

先看看通过序列化破坏单例的例子,其中 Singleton 实现了 Serializable 接口,才有可能通过序列化破坏单例。

public class DobleCheckSingleton implements Serializable {

    private DobleCheckSingleton() {
    }

    private static volatile DobleCheckSingleton dobleCheckSingleton;

    public static DobleCheckSingleton getSingleton() {
        if (dobleCheckSingleton == null) {
            synchronized (DobleCheckSingleton.class) {
                if (dobleCheckSingleton == null) {
                    dobleCheckSingleton = new DobleCheckSingleton();
                }
            }
        }
        return dobleCheckSingleton;
    }

    public static void main(String[] args) {
        try {
            DobleCheckSingleton dobleCheckSingleton = DobleCheckSingleton.getSingleton();

            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
            oos.writeObject(dobleCheckSingleton);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SerSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            DobleCheckSingleton s1 = (DobleCheckSingleton) ois.readObject();
            ois.close();

            System.out.println(dobleCheckSingleton == s1);
   } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
输出:false,单例被破坏
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

枚举类实现,枚举类不实现 Serializable 接口,都可以进行序列化,并且返回原来的单例。

public enum EnumSingleton {

    INSTANCE;

    public static void main(String[] args) {
        try {
            EnumSingleton enumSingleton = EnumSingleton.INSTANCE;

            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
            oos.writeObject(enumSingleton);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SerSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            EnumSingleton s1 = (EnumSingleton) ois.readObject();
            ois.close();

            System.out.println(enumSingleton == s1);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:true
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

原因: 枚举类的 writeObject 方法仅仅是将 Enum.name 写到文件中,反序列化时,根据 readObject 方法的源码定位到 Enum 的 valueOf 方法,他会根据名称返回原来的对象。

克隆

实现 Cloneable 接口重写 clone 方法,但是 Enum 类中 clone 的方法是 final 类型,无法重写,也就不能通过克隆破坏单例。

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
     protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

不用枚举如何防止单例模式破坏

若实现了序列化接口,重写 readResolve 方法即可,反序列化时将调用该方法返回对象实例。

public Object readResolve() throws ObjectStreamException {
    return dobleCheckSingleton;
}
  • 1.
  • 2.
  • 3.

通过反射破坏单例的场景,可以在构造方法中判断实例是否已经创建,若已创建则抛出异常。

private Singleton(){
    if (instance !=null){
        throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

通过clone破坏单例的场景,可以重写clone方法,返回已有单例对象。


责任编辑:武晓燕 来源: seven97
相关推荐

2023-08-04 08:00:00

ControlNet医学图像

2024-03-18 00:09:19

人工智能生成式人工智能安全

2023-07-07 11:24:04

2024-10-30 08:31:36

Next.js高效性能

2012-10-25 13:46:42

2021-10-15 11:28:06

物联网边缘计算IoT

2023-05-11 14:07:29

2024-07-17 08:27:29

2019-07-25 06:49:26

2013-08-14 10:43:37

2020-08-19 09:45:10

IBMAIOps混合多云管理

2024-02-23 16:12:47

2011-12-28 21:12:10

移动支付

2020-11-19 17:36:10

IT 运营

2023-06-02 10:36:59

2024-10-12 08:35:32

2022-09-30 14:32:23

人工智能数据隐私游戏规则

2021-01-28 12:37:40

物联网体育行业IOT

2023-03-21 08:02:34

架构React服务器

2016-09-10 08:20:09

IBM
点赞
收藏

51CTO技术栈公众号