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

簡介

序列化是java中一個很是經常使用又會被人忽視的功能,咱們將對象寫入文件須要序列化,同時,對象若是想要在網絡上傳輸也須要進行序列化。java

序列化的目的就是保證對象能夠正確的傳輸,那麼咱們在序列化的過程當中須要注意些什麼問題呢?git

一塊兒來看看吧。github

序列化簡介

若是一個對象要想實現序列化,只須要實現Serializable接口便可。網絡

奇怪的是Serializable是一個不須要任何實現的接口。若是咱們implements Serializable可是不重寫任何方法,那麼將會使用JDK自帶的序列化格式。函數

可是若是class發送變化,好比增長了字段,那麼默認的序列化格式就知足不了咱們的需求了,這時候咱們須要考慮使用本身的序列化方式。this

若是類中的字段不想被序列化,那麼可使用transient關鍵字。代理

一樣的,static表示的是類變量,也不須要被序列化。code

注意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/

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

歡迎關注個人公衆號:「程序那些事」,懂技術,更懂你!

相關文章
相關標籤/搜索