天天看點

反射破壞單例模式以及怎樣防禦

先貼幾個比較全的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個執行個體

反射破壞單例模式以及怎樣防禦

繼續閱讀