開發與維運

java安全編碼指南之:序列化Serialization

簡介

序列化是java中一個非常常用又會被人忽視的功能,我們將對象寫入文件需要序列化,同時,對象如果想要在網絡上傳輸也需要進行序列化。

序列化的目的就是保證對象可以正確的傳輸,那麼我們在序列化的過程中需要注意些什麼問題呢?

一起來看看吧。

序列化簡介

如果一個對象要想實現序列化,只需要實現Serializable接口即可。

奇怪的是Serializable是一個不需要任何實現的接口。如果我們implements Serializable但是不重寫任何方法,那麼將會使用JDK自帶的序列化格式。

但是如果class發送變化,比如增加了字段,那麼默認的序列化格式就滿足不了我們的需求了,這時候我們需要考慮使用自己的序列化方式。

如果類中的字段不想被序列化,那麼可以使用transient關鍵字。

同樣的,static表示的是類變量,也不需要被序列化。

注意serialVersionUID

serialVersionUID 表示的是對象的序列ID,如果我們不指定的話,是JVM自動生成的。在反序列化的過程中,JVM會首先判斷serialVersionUID 是否一致,如果不一致,那麼JVM會認為這不是同一個對象。

如果我們的實例在後期需要被修改的話,注意一定不要使用默認的serialVersionUID,否則後期class發送變化之後,serialVersionUID也會同樣的發生變化,最終導致和之前的序列化版本不兼容。

writeObject和readObject

如果要自己實現序列化,那麼可以重寫writeObject和readObject兩個方法。

注意,這兩個方法是private的,並且是non-static的:

private void writeObject(final ObjectOutputStream stream)
    throws IOException {
  stream.defaultWriteObject();
}
 
private void readObject(final ObjectInputStream stream)
    throws IOException, ClassNotFoundException {
  stream.defaultReadObject();
}

如果不是private和non-static的,那麼JVM就不能夠發現這兩個方法,就不會使用他們來做自定義序列化。

readResolve和writeReplace

如果class中的字段比較多,而這些字段都可以從其中的某一個字段中自動生成,那麼我們其實並不需要序列化所有的字段,我們只把那一個字段序列化就可以了,其他的字段可以從該字段衍生得到。

readResolve和writeReplace就是序列化對象的代理功能。

首先,序列化對象需要實現writeReplace方法,表示替換成真正想要寫入的對象:

public class CustUserV3 implements java.io.Serializable{

    private String name;
    private String address;

    private Object writeReplace()
            throws java.io.ObjectStreamException
    {
        log.info("writeReplace {}",this);
        return new CustUserV3Proxy(this);
    }
}

然後在Proxy對象中,需要實現readResolve方法,用於從系列化過的數據中重構序列化對象。如下所示:

public class CustUserV3Proxy implements java.io.Serializable{

    private String data;

    public CustUserV3Proxy(CustUserV3 custUserV3){
        data =custUserV3.getName()+ "," + custUserV3.getAddress();
    }

    private Object readResolve()
            throws java.io.ObjectStreamException
    {
        String[] pieces = data.split(",");
        CustUserV3 result = new CustUserV3(pieces[0], pieces[1]);
        log.info("readResolve {}",result);
        return result;
    }
}

我們看下怎麼使用:

public void testCusUserV3() throws IOException, ClassNotFoundException {
        CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com");

        try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(custUserA);
        }

        try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject();
            log.info("{}",custUser1);
        }
    }

注意,我們寫入和讀出的都是CustUserV3對象。

不要序列化內部類

所謂內部類就是未顯式或隱式聲明為靜態的嵌套類,為什麼我們不要序列化內部類呢?

  • 序列化在非靜態上下文中聲明的內部類,該內部類包含對封閉類實例的隱式非瞬態引用,從而導致對其關聯的外部類實例的序列化。
  • Java編譯器對內部類的實現在不同的編譯器之間可能有所不同。從而導致不同版本的兼容性問題。
  • 因為Externalizable的對象需要一個無參的構造函數。但是內部類的構造函數是和外部類的實例相關聯的,所以它們無法實現Externalizable。

所以下面的做法是正確的:

public class OuterSer implements Serializable {
  private int rank;
  class InnerSer {
    protected String name;
  }
}

如果你真的想序列化內部類,那麼把內部類置為static吧。

如果類中有自定義變量,那麼不要使用默認的序列化

如果是Serializable的序列化,在反序列化的時候是不會執行構造函數的。所以,如果我們在構造函數或者其他的方法中對類中的變量有一定的約束範圍的話,反序列化的過程中也必須要加上這些約束,否則就會導致惡意的字段範圍。

我們舉幾個例子:

public class SingletonObject implements Serializable {
    private static final SingletonObject INSTANCE = new SingletonObject ();
    public static SingletonObject getInstance() {
        return INSTANCE;
    }
    private SingletonObject() {
    }

    public static Object deepCopy(Object obj) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            new ObjectOutputStream(bos).writeObject(obj);
            ByteArrayInputStream bin =
                    new ByteArrayInputStream(bos.toByteArray());
            return new ObjectInputStream(bin).readObject();
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static void main(String[] args) {
        SingletonObject singletonObject= (SingletonObject) deepCopy(SingletonObject.getInstance());
        System.out.println(singletonObject == SingletonObject.getInstance());
    }
}

上面是一個singleton對象的例子,我們在其中定義了一個deepCopy的方法,通過序列化來對對象進行拷貝,但是拷貝出來的是一個新的對象,儘管我們定義的是singleton對象,最後運行的結果還是false,這就意味著我們的系統生成了一個不一樣的對象。

怎麼解決這個問題呢?

加上一個readResolve方法就可以了:

    protected final Object readResolve() throws NotSerializableException {
        return INSTANCE;
    }

在這個readResolve方法中,我們返回了INSTANCE,以確保其是同一個對象。

還有一種情況是類中字段是有範圍的。

public class FieldRangeObject implements Serializable {

    private int age;

    public FieldRangeObject(int age){
        if(age < 0 || age > 100){
            throw new IllegalArgumentException("age範圍不對");
        }
        this.age=age;
    }
}

上面的類在反序列化中會有什麼問題呢?

因為上面的類在反序列化的過程中,並沒有對age字段進行校驗,所以,惡意代碼可能會生成超出範圍的age數據,當反序列化之後就溢出了。

怎麼處理呢?

很簡單,我們在readObject方法中進行範圍的判斷即可:

    private  void readObject(java.io.ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField fields = s.readFields();
        int age = fields.get("age", 0);
        if (age > 100 || age < 0) {
            throw new InvalidObjectException("age範圍不對!");
        }
        this.age = age;
    }

不要在readObject中調用可重寫的方法

為什麼呢?readObject實際上是反序列化的構造函數,在readObject方法沒有結束之前,對象是沒有構建完成,或者說是部分構建完成。如果readObject調用了可重寫的方法,那麼惡意代碼就可以在方法的重寫中獲取到還未完全實例化的對象,可能造成問題。

本文的代碼:

learn-java-base-9-to-20/tree/master/security

本文已收錄於 http://www.flydean.com/java-security-code-line-serialization/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!

Leave a Reply

Your email address will not be published. Required fields are marked *