天天看點

java安全編碼指南之:序列化Serialization簡介序列化簡介注意serialVersionUIDwriteObject和readObjectreadResolve和writeReplace不要序列化内部類如果類中有自定義變量,那麼不要使用預設的序列化不要在readObject中調用可重寫的方法

簡介

序列化是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/

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

歡迎關注我的公衆号:「程式那些事」,懂技術,更懂你!