反射破坏单例模式以及怎样防御

先贴几个比较全的java反射博客

  1. Java反射:用最直接的大白话来聊一聊Java中的反射机制
  2. Java基础之—反射(非常重要)
  3. Java反射技术详解

前面介绍的单例模式的实现方式: 设计模式之单例模式

单例模式的设计在于只保留一个公有静态函数来获取唯一的实例,其他方法(构造函数)或字段为私有,外界不能访问。

而java反射则突破了构造函数私有的限制,可以获取单例类的私有构造函数并使用。

//得到该类在内存中的字节码对象
Class objectClass = Class.forName("com.xt.designmode.creational.singleton.hungryBoyClass.Singleton");
//获取构造器
Constructor constructor = objectClass.getDeclaredConstructor();
//暴力反射,解除私有限定
constructor.setAccessible(true);
Singleton reflectInstance = (Singleton) constructor.newInstance();

我们针对所有实现单例模式的方法使用如下测试

public class ReflectTest {
    
    
    
    public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException {
    
    
        Class objectClass = Class.forName("com.xt.designmode.creational.singleton.hungryBoyClass.Singleton");

        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);

        Singleton instance = Singleton.getInstance();
        Singleton reflectInstance = (Singleton) constructor.newInstance();

        System.out.println(instance);
        System.out.println(reflectInstance);

        //对象类型, == 比较的是地址
        if(instance != reflectInstance){
    
    
            System.out.printf("创建了两个实例\n");
        }else{
    
    
            System.out.printf("只创建了一个实例\n");
        }

    }
}

除开枚举式实现的单例模式,其余的方式都可以被反射直接获取到私有的构造器来创建新实例。

我们先看看枚举类是怎样防御反射攻击的。
像我 设计模式之单例模式这篇博客实现的枚举类实现单例是不能抵抗反射攻击的,因为我们不反射枚举类Singleton,直接反射Resource类就可以了,并且因为Resource类的构造函数是public的,所以真想攻击的话,直接去new Resource实例就好了。(逃

public enum Singleton {
    
    

    INSTANCE;
    private Resource resource;

    private Singleton(){
    
    
        resource = new Resource();
    }

    public Resource getInstance(){
    
    
        return resource;
    }
    
}

我们采用如下形式enum 枚举类实现单例模式

public enum EnumSingleton {
    
    

    INSTANCE;
    private EnumSingleton(){
    
    
    }

    public static EnumSingleton getInstance() {
    
    
        return INSTANCE;
    }
}

测试

public class ReflectTest {
    
    

    public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException {
    
    
        Class objectClass = Class.forName("com.xt.designmode.creational.singleton.destroy.EnumSingleton");
        
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);

        //枚举类式的单例模式
        EnumSingleton instance = EnumSingleton.INSTANCE;
        EnumSingleton reflectInstance = (EnumSingleton) constructor.newInstance();
        if(instance != reflectInstance){
    
    
            System.out.printf("创建了两个实例\n");
        }else{
    
    
            System.out.printf("只创建了一个实例\n");
        }
    }
}

在这里插入图片描述
会报出找不到无参的构造方法的异常。这个问题多个博主也遇到过,通过反编译的方式找到了问题所在。

我们使用javac来对代码进行编译,使用jad工具对class代码进行反编译

javac工具读由java语言编写的类和接口的定义,并将它们编译成字节代码的class文件
运用cmd终端或者IDEA终端 cd 到要编译文件的所在文件夹下面,运行以下命令将EnumSingleton.java编译成EnumSingleton.class文件

javac EnumSingleton.java

然后使用jad 工具来对class文件进行反编译
jad下载地址
我这里下载的是windows版本,然后放到一个文件夹下面,最后将文件夹位置设置到系统的环境变量。如下所示,是我的jad.exe所在的文件夹路径
在这里插入图片描述
将EnumSingleton.class文件复制到一个指定的文件夹下面,不然在源代码文件夹下,使用jad反编译生成的.java文件会覆盖原来的文件

jad -sjava EnumSingleton.class

生成的反编译代码如下所示:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumSingleton.java

package com.xt.designmode.creational.singleton.destroy;
public final class EnumSingleton extends Enum
{
    
    

    public static EnumSingleton[] values()
    {
    
    
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String s)
    {
    
    
        return (EnumSingleton)Enum.valueOf(com/xt/designmode/creational/singleton/destroy/EnumSingleton, s);
    }

    private EnumSingleton(String s, int i)
    {
    
    
        super(s, i);
    }

    public static EnumSingleton getInstance()
    {
    
    
        return INSTANCE;
    }

    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton $VALUES[];

    static 
    {
    
    
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
    
    
            INSTANCE
        });
    }
}

会发现枚举类内部其实是一个有参的构造函数。

也可以通过反射获取所有的构造方法,打印出来看一看,瞧一瞧

  Constructor<?>[] cons=objectClass.getDeclaredConstructors();
  for(Constructor<?>con:cons) {
    
    
      System.out.println("构造方法:"+con);
  }

如下所示,和上面反编译的代码一样。只有个String和int参数的有参构造器
在这里插入图片描述
所以我们获取其带有String和int参数的有参构造

Constructor constructor = objectClass.getDeclaredConstructor(String.class,int.class);

然后再通过这个构造器new一个实例
在这里插入图片描述

抛出异常,不能通过反射创建枚举类对象

debug进去看是那个地方抛出的异常,可以看见在反射的newInstance()方法里面,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败
有如下方法,对于枚举类来说,通过反射创建枚举类实例的路是堵死了,所以枚举类实现单例模式不用怕反射破坏。

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
    
    
         //其他代码已删除
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
         //其他代码已删除
       
    }

那么其他单例模式的实现方式可以怎么防御反射攻击呢?

既然反射是通过暴力获取单例类的私有构造器来构造新实例,那就从构造器入手

饿汉式

    private Singleton(){
    
    
        if(singleton!=null){
    
    
            throw new RuntimeException("单例模式下,禁止使用反射创建新实例");
        }
    }

静态内部类式

    private Singleton(){
    
    
    
        if(InnerClass.singleton!=null){
    
    
            throw new RuntimeException("单例模式下,禁止使用反射创建新实例");
        }
    }

这两种方式通过更改构造器的代码都可以有效防止反射创建新实例

我们接下来对用双重检查锁式实现的单例模式进行测试。为了展示懒汉式使用(改动构造函数来防止反射攻击)方法的漏洞。我将Singleton类引用变量singleton设置成了public,方便测试代码获取打印到控制台

public class Singleton {
    
    

    public static volatile Singleton singleton;

    private Singleton() {
    
    
        if(singleton!=null){
    
    
            throw new RuntimeException("单例模式下,禁止使用反射创建新实例");
        }
    }

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

测试代码如下,有详细的注释

public class ReflectTest {
    
    

    public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException {
    
    
        //得到该类在内存中的字节码对象
        Class objectClass = Class.forName("com.xt.designmode.creational.singleton.doubleCheckClass.Singleton");
        //获取这个对象的构造器
        Constructor constructor = objectClass.getDeclaredConstructor();
        //暴力反射,解除私有限定
        constructor.setAccessible(true);
		//先使用反射获取的构造器创建一个实例
        Singleton reflectInstance = (Singleton) constructor.newInstance();
        //打印反射机制创造的实例
        System.out.println(reflectInstance);
		//打印此时单例类中的Singleton类引用变量singleton
        System.out.println(Singleton.singleton);
		//使用常规方式获取实例
        Singleton instance = Singleton.getInstance();
        //打印常规方式生成的实例
        System.out.println(instance);

        //对象类型, == 比较的是地址
        if(instance != reflectInstance){
    
    
            System.out.printf("创建了两个实例\n");
        }else{
    
    
            System.out.printf("只创建了一个实例\n");
        }
    }
}

如下图所示,创建了两个实例,是因为正常的双重检查锁式只是想实例化单例类中的一个引用变量,如果在单例类还没有实例化这个引用变量singleton之前,反射就可以通过使用构造器创建无数个实例,直到有人使用常规的方式来将单例类中的引用变量实例化。
本来懒汉式只是为了节约资源,等到真正有人使用他的时候才创建,但是没想到给反射钻了空子。
在这里插入图片描述
既然他钻这个空子,我们就想别的办法,因为我们只需要一个实例,而懒汉式的实例都是通过构造函数来创建的,那我们只需要保证构造函数只会被调用一次就好了。我们加个标记flag

public class Singleton {
    
    
    private static volatile Singleton singleton;
    private static boolean flag = true;

    private Singleton() {
    
    
       
        if(flag == true){
    
    
            flag = false;
        }else{
    
    
            throw new RuntimeException("单例模式下,禁止使用反射创建新实例");
        }
    }

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

这样就限制了构造函数的使用次数,可以有效防止反射通过获取构造器来创建新实例。
但是反射是可以获取指定类的一切属性和方法,所以也可以通过反射不断将flag置为true来破解这一限制

public class ReflectTest {
    
    

    public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException, NoSuchFieldException {
    
    
        //得到该类在内存中的字节码对象
        Class objectClass = Class.forName("com.xt.designmode.creational.singleton.doubleCheckClass.Singleton");
        //获取这个对象的构造器
        Constructor constructor = objectClass.getDeclaredConstructor();
        //暴力反射,解除私有限定
        constructor.setAccessible(true);

        for(int i=0;i<10;i++){
    
    
            Singleton reflectInstance = (Singleton) constructor.newInstance();
            Field flag = objectClass.getDeclaredField("flag");
            flag.setAccessible(true);
            flag.set(reflectInstance, true);
            System.out.println(reflectInstance);
        }

    }
}

如下图所示,我们创建了10个实例
在这里插入图片描述
所以这些方法都不能防范反射破坏,只有从反射的newInstance()方法入手,让这个方法不能运行成功,也就是使用上面所说的枚举式单例模式。

有错误以及不足的地方,请各位大佬斧正,哈利嘎多。

References:

  1. https://www.cnblogs.com/henuliulei/p/14870622.html
  2. https://blog.csdn.net/qq_37960603/article/details/104076323

Guess you like

Origin blog.csdn.net/qq_37774171/article/details/121729609