java序列化知多少

原文鏈接:http://www.blog.evanzhou.top/2020/05/23/xuliehuaxieyiguifanjishenrulejiexuliehua/

1 序列化概念

大多數小夥伴肯定知道什麼是序列化啦!也不用我多說。序列化主要有兩個作用:對象持久化網絡間傳輸。對象持久化例如,把對象轉換成二進制流寫入到磁盤上。網絡間傳輸,在rpc框架間用的很廣泛。

  • 序列化:把對象編碼成二進制流的過程叫做序列化。
  • 反序列化:把對象二進制流轉換爲對象的過程叫做反序列化。

2 如何運用好序列化

“如何運用好序列化”看到這個標題你會很詫異,"呵,這還不簡單,實現Serializable接口不就完事了嗎,很難嗎?“三年前我那會剛畢業出來找工作的時候,面試官問我"你是如何實現序列化的”,我:“這還不簡單嗎?實現Serializable接口不就完了嗎?” 結果可想而知 。現在回想爲什麼我當時會這樣認爲呢?大多數情況下,是從數據庫查出數據,轉換成VO返回到前端頁面就完事了。VO類實現序列化,然後緩存到redis裏,在這種情況下,一般來說是問題不大的,因爲我們的VO類等同於邏輯內容,VO類基本上是一些屬性值和get/set方法,比如人名,手機號,地址等,這些屬性本來就是API接口要提供的,所以暴露出去不成問題。

實現序列化接口的代價:

  • 破壞了了對象的封裝性。如果我們只實現了Serializable接口,類中的所有實例域都會被導出,包括私有的屬性,類設計的目標是“最低限度的訪問域”,類裏面的一些自有的域對訪問者是不可直接訪問的。
  • 增加了bug和出現安全漏洞的可能性
    序列化機制是一種語言之外的的對象創建機制,反序列化機制是一個“隱藏的構造器”,具備與其它構造器相同的特點。
  • java的默認的序列化方式碼流十分龐大,性能十分差。
  • java默認的序列化方式不支持跨語言和跨平臺。
    關於第三點和第四點以後再說。

3 是否需要序列化的一些準則

  • 爲了繼承而設計的類,應該儘可能的少實現Serializable接口。
  • 如果父類不是可序列化的,那麼也無法編寫可序列化的子類。特別是,父類在沒有提供無參構造器的情況下。
  • 在“允許子類序列化”和“不允許子類序列化”之間存在一個折中的方案,父類提供一個無參的構造器,由子類自己決定是否實現序列化。

3.1 爲了繼承而設計的類,應該儘可能的少實現Serializable接口

爲什麼說“爲了繼承而設計的類,應該儘可能的少實現Serializable接口”呢。如果父類實現了序列化,那麼子類中的屬性也會被序列化,因此程序員在開發子類的時候,就必須考慮哪些東西不該暴露出去,序列化會暴露對象內部的實例域,包括私有的實例域,因此這會增加開發人員的負擔。

3.2 如果父類不是可序列化的,那麼也無法編寫可序列化的子類。特別是,父類在沒有提供無參構造器的情況下。

“如果父類不是可序列化的,那麼也無法編寫可序列化的子類。特別是,父類在沒有提供無參構造器的情況下”。如果我們專爲繼承實現了一些類,這裏類需要子類繼承並實現,而我們開發的子類又有序列化的要求,這是如果父類不可序列化,那麼我們無法編寫可序列化的子類,並不是說會報錯,而是反序列化後父類中的實例域都會丟失,只有在父類沒有提供無參構造器的情況下,纔會反序列化失敗報錯。以下代碼說明這一情況:

Parent是爲了繼承而設計的抽象類,需要子類自己繼承該類並實現該類的抽象方法(該示例代碼中沒有體現,也不需要),下面的Parent類沒有實現序列化接口,也就不是可序列化的,父類的屬性在序列化的時候會丟失。

public abstract class Parent {
	private String name;

	private Date date;

	public Parent() {
		this.name = name;
	}

	public Parent(String name) {
		this.name = name;
	}

	public Parent(String name, Date date) {
		this.name = name;
		this.date = date;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Date getDate() {
		return date;
	}

	public void setDate(Date date) {
		this.date = date;
	}
}

下面我們有一個Child類,繼承了抽象父類Parent,實現了父類的一些方法以滿足需求(該類並沒有也不需要體現,我們的主要關注點在序列化上)。

public class Child extends Parent implements Serializable {

    private static final long serialVersionUID = 1L;
    private String sex;

    @Override
    public String toString() {
        return "Child{" + "sex='" + sex + '\'' + ", name='" + super.getName() + '\'' + ", date=" + super.getDate()
            + '}';
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public Child() {
        super("我是你爸爸", new Date());
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Child child = new Child();
        child.setSex("男");

        System.out.println("序列化之前對象狀態:");
        System.out.println(child.toString());

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(child);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Child deserier = (Child)objectInputStream.readObject();
        System.out.println("反序列化之後對象狀態:");
        System.out.println(deserier);

    }
}

運行結果:
在這裏插入圖片描述
可以看到父類實例域相關的值都是空的,如果我們的子類實現過程中依賴於這些狀態值,但又取不到值,那麼僅子類序列化是無意義的。
假如我們去掉父類的無參構造器,儘管子類調用的是父類的有參構造器,反序列化也不會成功,運行時會報InvalidClassException異常,提示沒有有效的構造器。
在這裏插入圖片描述

3.3 在“允許子類序列化”和“不允許子類序列化”的折中方案

基於3.1和3.2的討論,我們在設計抽象類的時候,並不確定子類需不要序列化,因此通常我們需要提供無參的構造器,由子類自己決定時候需要序列化,子類如果需要序列化,所做的工作只是實現Serialible接口,編寫readObject 和writeObject方法,視決定自己需要序列化或者反序列化哪些屬性。示例代碼:

public class Child extends Parent implements Serializable {

    private static final long serialVersionUID = 1L;
    private String sex;

// 與3.2相比多了這一個方法
    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
        inputStream.defaultReadObject();
    }

    @Override
    public String toString() {
        return "Child{" + "sex='" + sex + '\'' + ", name='" + super.getName() + '\'' + ", date=" + super.getDate()
            + '}';
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public Child() {
        super("我是你爸爸", new Date());
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Child child = new Child();
        child.setSex("男");

        System.out.println("序列化之前對象狀態:");
        System.out.println(child.toString());

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(child);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Child deserier = (Child)objectInputStream.readObject();
        System.out.println("反序列化之後對象狀態:");
        System.out.println(deserier);

    }

    public static String bytesToHexString(byte[] bytes) {
        if (bytes == null)
            return null;
        StringBuffer stringBuffer = new StringBuffer(bytes.length * 2);
        for (byte b : bytes) {
            if ((b & 0xFF) < 0x10) {
                stringBuffer.append('0');
            }
            stringBuffer.append(Integer.toHexString(0xFF & b));
            stringBuffer.append(" ");
        }

        return stringBuffer.toString().toUpperCase(Locale.getDefault());
    }

運行結果:
在這裏插入圖片描述
可以看到相比3.2,反序列化之後,name和date實例域也是有值的。

4 序列化安全性問題

4.1 序列化規範

在探討如何保證序列化安全性問題之前,我們先看一個例子。在演示這個例子之前,我還得介紹寫關於序列化協議的知識。如何讓你來從底層實現序列化協議,那麼你肯定會考慮如何去描述一個對象,需要有一種基礎結構描述對象的類,如果是動態代理類如何處理、對象的屬性、對象的引用、引用類型、空對象如何描述、循環引用怎麼處理,重複引用某個對象怎麼處理等等,這些東西還是很複雜的。JDK官方有一套序列化語法規則:

stream:
  magic version contents

contents:
  content
  contents content

content:
  object
  blockdata

object:
  newObject
  newClass
  newArray
  newString
  newEnum
  newClassDesc
  prevObject
  nullReference
  exception
  TC_RESET

newClass:
  TC_CLASS classDesc newHandle

classDesc:
  newClassDesc
  nullReference
  (ClassDesc)prevObject      // an object required to be of type
                             // ClassDesc

superClassDesc:
  classDesc

newClassDesc:
  TC_CLASSDESC className serialVersionUID newHandle classDescInfo
  TC_PROXYCLASSDESC newHandle proxyClassDescInfo
classDescInfo:
  classDescFlags fields classAnnotation superClassDesc 

className:
  (utf)

serialVersionUID:
  (long)

classDescFlags:
  (byte)                  // Defined in Terminal Symbols and
                            // Constants

proxyClassDescInfo:
  (int)<count> proxyInterfaceName[count] classAnnotation
      superClassDesc
proxyInterfaceName:
  (utf)
fields:
  (short)<count>  fieldDesc[count]

fieldDesc:
  primitiveDesc
  objectDesc

primitiveDesc:
  prim_typecode fieldName

objectDesc:
  obj_typecode fieldName className1

fieldName:
  (utf)

className1:
  (String)object             // String containing the field's type,
                             // in field descriptor format
classAnnotation:
  endBlockData
  contents endBlockData      // contents written by annotateClass

prim_typecode:
  `B'       // byte
  `C'       // char
  `D'       // double
  `F'       // float
  `I'       // integer
  `J'       // long
  `S'       // short
  `Z'       // boolean

obj_typecode:
  `[`   // array
  `L'       // object

newArray:
  TC_ARRAY classDesc newHandle (int)<size> values[size]

newObject:
  TC_OBJECT classDesc newHandle classdata[]  // data for each class

classdata:
  nowrclass                 // SC_SERIALIZABLE & classDescFlag &&
                            // !(SC_WRITE_METHOD & classDescFlags)
  wrclass objectAnnotation  // SC_SERIALIZABLE & classDescFlag &&
                            // SC_WRITE_METHOD & classDescFlags
  externalContents          // SC_EXTERNALIZABLE & classDescFlag &&
                            // !(SC_BLOCKDATA  & classDescFlags
  objectAnnotation          // SC_EXTERNALIZABLE & classDescFlag&& 
                            // SC_BLOCKDATA & classDescFlags

nowrclass:
  values                    // fields in order of class descriptor

wrclass:
  nowrclass

objectAnnotation:
  endBlockData
  contents endBlockData     // contents written by writeObject
                            // or writeExternal PROTOCOL_VERSION_2.

blockdata:
  blockdatashort
  blockdatalong

blockdatashort:
  TC_BLOCKDATA (unsigned byte)<size> (byte)[size]

blockdatalong:
  TC_BLOCKDATALONG (int)<size> (byte)[size]

endBlockData   :
  TC_ENDBLOCKDATA

externalContent:          // Only parseable by readExternal
  ( bytes)                // primitive data
    object

externalContents:         // externalContent written by 
  externalContent         // writeExternal in PROTOCOL_VERSION_1.
  externalContents externalContent

newString:
  TC_STRING newHandle (utf)
  TC_LONGSTRING newHandle (long-utf)

newEnum:
  TC_ENUM classDesc newHandle enumConstantName
enumConstantName:
  (String)object
prevObject
  TC_REFERENCE (int)handle

nullReference
  TC_NULL

exception:
  TC_EXCEPTION reset (Throwable)object         reset 

magic:
  STREAM_MAGIC

version
  STREAM_VERSION

values:          // The size and types are described by the
                 // classDesc for the current object

newHandle:       // The next number in sequence is assigned
                 // to the object being serialized or deserialized

reset:           // The set of known objects is discarded
                 // so the objects of the exception do not
                 // overlap with the previously sent objects 
                 // or with objects that may be sent after 
                 // the exception

備註: 如何看懂這套語法圖呢?首先冒號左邊的是解釋說明,右邊的纔是流裏面的內容,看懂這副語法圖,我們需要用遞歸的思維去看,比如在語法樹開頭部分我們讀到了“magic”,那麼我們繼續向下搜索“maigic”,直到找到終端標誌(可以理解爲語法規則裏的常量)爲止,在這裏我們讀到STREAM_MAGICz終端標誌,查看終端標誌表,發現是magic魔數,聲明使用序列化協議,沒有太大含義,就跟class文件以CAFEBABY開頭一樣。

所有的終端標誌如下:

final static short STREAM_MAGIC = (short)0xaced;
    final static short STREAM_VERSION = 5;
    final static byte TC_NULL = (byte)0x70;
    final static byte TC_REFERENCE = (byte)0x71;
    final static byte TC_CLASSDESC = (byte)0x72;
    final static byte TC_OBJECT = (byte)0x73;
    final static byte TC_STRING = (byte)0x74;
    final static byte TC_ARRAY = (byte)0x75;
    final static byte TC_CLASS = (byte)0x76;
    final static byte TC_BLOCKDATA = (byte)0x77;
    final static byte TC_ENDBLOCKDATA = (byte)0x78;
    final static byte TC_RESET = (byte)0x79;
    final static byte TC_BLOCKDATALONG = (byte)0x7A;
    final static byte TC_EXCEPTION = (byte)0x7B;
    final static byte TC_LONGSTRING = (byte) 0x7C;
    final static byte TC_PROXYCLASSDESC = (byte) 0x7D;
    final static byte TC_ENUM = (byte) 0x7E;
    final static  int   baseWireHandle = 0x7E0000;

示例:

public class List implements java.io.Serializable {
	int value;
	List next;

	public static void main(String[] args) {
		try {
			List list1 = new List();
			List list2 = new List();
			list1.value = 17;
			list1.next = list2;
			list2.value = 19;
			list2.next = null;

			FileOutputStream o = new FileOutputStream("1.txt");
			ObjectOutputStream out = new ObjectOutputStream(o);
			out.writeObject(list1);
			out.writeObject(list2);
			out.flush();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
}

打開文件1.txt:

aced 0005 7372 0012 636f 6d2e 6769 6d63
2e74 6573 742e 4c69 7374 4127 d6e5 a5d0
5a8d 0200 0249 0005 7661 6c75 654c 0004
6e65 7874 7400 144c 636f 6d2f 6769 6d63
2f74 6573 742f 4c69 7374 3b78 7000 0000
1173 7100 7e00 0000 0000 1370 7100 7e00
03

4.2 演示

廢話不多說,現在演示安全性攻擊問題:
Parent:這是我們父類實現了序列化接口 包含name屬性和date屬性

public class Parent implements Serializable {
	private String name;
	private Date date;
	// 此處省略所有的構造方法
	// 此處省略protected 的get/set方法
	//set date私有
	private void setDate(Date date) {
		this.date = date;
	}
}
public class Child  extends Parent implements Serializable {
	private static final long serialVersionUID = 1L;
	private String sex;
    // 此處省略get/set方法
	public Child() {
		super("我是你爸爸", new Date());
	}

	@Override
	public String toString() {
		return "Child{" + "sex='" + sex + '\'' + ", name='" + super.getName() + '\'' + ", date=" + super.getDate()
				+ '}';
	}
	
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		Child child = new Child();
		child.setSex("男");

		System.out.println("序列化之前對象狀態: " + child);
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
		objectOutputStream.writeObject(child);

		byte[] ref = {0x71, 0, 0x7e, 0, 6};
		byteArrayOutputStream.write(ref);
		ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
		ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
		Child deserier = (Child)objectInputStream.readObject();
		System.out.println("反序列化之後對象狀態: " + deserier);

		// 拿到反序列化對象中的內部對象
		Date date = (Date)objectInputStream.readObject();
		System.out.println(date);
		date.setYear(110);
		System.out.println(deserier);

	}
}

Child是我們子類繼承Parent並且實現了序列化接口,main方法中首先我們構造了合法的字節流,接着我們僞造字節流,插入“0x71, 0, 0x7e, 0, 6”,查詢語法表可知,0x71 含義如下:

prevObject
  TC_REFERENCE (int)handle

代表這是一個引用,指向之前的一個對象,爲了能夠方便的找到之前的對象,序列化引入了handle的概念,類似於句柄,handle是32位的整型,通過該值能夠迅速找到之前的對象,handle起始值必須從0x7E0000 開始。在這個例子中我門的handle值是0x007e0006,可以理解爲引用的是第六號“對象”,這裏的“對象”不完全是域對象,也指代域對象的描述符,比如“Ljava/lang/String”。

A basic structure is needed to represent objects in a stream. Each attribute of the object needs to be represented: its classes, its fields, and data written and later read by class-specific methods. The representation of objects in the stream can be described with a grammar. There are special representations for null objects, new objects, classes, arrays, strings, and back references to any object already in the stream. Each object written to the stream is assigned a handle that is used to refer back to the object. Handles are assigned sequentially starting from 0x7E0000. The handles restart at 0x7E0000 when the stream is reset.

運行結果:

序列化之前對象狀態: Child{sex='男', name='我是你爸爸', date=Sat May 23 15:39:25 GMT+08:00 2020}
反序列化之後對象狀態: Child{sex='男', name='我是你爸爸', date=Sat May 23 15:39:25 GMT+08:00 2020}
拿到反序列化對象內部的私有屬性Sat May 23 15:39:25 GMT+08:00 2020
Child{sex='男', name='我是你爸爸', date=Sun May 23 15:39:25 GMT+08:00 2010}

查看運行結果可以清楚的看到,通過僞造流,我們拿到了對象的私有域,並且改變了私有域的狀態,date本應該是2020年的,卻被我們強行改造爲2010年,這是很嚴重的安全問題。
要解決這個問題也很簡單,在Parent父類裏編寫保護性的readObject()方法。

	private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
		inputStream.defaultReadObject();
		date = new Date(date.getTime());
	}

改造後的運行結果:

序列化之前對象狀態: Child{sex='男', name='我是你爸爸', date=Sat May 23 15:44:33 GMT+08:00 2020}
反序列化之後對象狀態: Child{sex='男', name='我是你爸爸', date=Sat May 23 15:44:33 GMT+08:00 2020}
拿到反序列化對象內部的私有屬性Sat May 23 15:44:33 GMT+08:00 2020
Child{sex='男', name='我是你爸爸', date=Sat May 23 15:44:33 GMT+08:00 2020}

5 關於序列化的安全性問題建議

5.1 反序列化時候別忘記校驗對象的有效性

當我們反序列化的時候,會從對象流讀取字節碼,構造新的對象,因此反序列化的時候,我們也要保證反序列化的對象和正常new出來的對象具有相同的約束,這樣做的目的是防止手工篡改過的流成功反序列化成“不合法”的對象,通常這樣的對象的內部狀態並不是我們期望的。因此,在readObject()方法裏,我們需要手動校驗對象的狀態是否合法。如果有效性校驗失敗,我們需要果斷拋出InvalidObjectException。

5.2 readObject方法的保護性

如果你覺得校驗了對象的有效性,就可以規避安全問題,那麼你就錯了,如上所述4.2

5.3 強烈建議不要序列化內部類(即非靜態成員類的嵌套類)

因爲在非靜態上下文中聲明的內部類包含對封閉類實例的隱式非瞬態引用,所以序列化此類內部類實例也將導致對其關聯的外部類實例進行序列化。由javac(或其他Java TM編譯器)生成的用於實現內部類的合成字段取決於實現,並且在編譯器之間可能有所不同。這些字段的差異可能會破壞兼容性,並導致默認衝突serialVersionUID價值觀。分配給本地和匿名內部類的名稱也取決於實現,並且在編譯器之間可能有所不同。由於內部類不能聲明除編譯時常量字段以外的靜態成員,因此內部類不能使用該 serialPersistentFields機制來指定可序列化的字段。最後,由於與外部實例相關聯的內部類沒有零參數構造函數(此類內部類的構造函數隱式接受封閉實例作爲前置參數),因此它們無法實現 Externalizable。但是,上面列出的所有問題均不適用於靜態成員類

5.4 readObject方法第一行最好先調用inputStream.defaultReadObject()方法;

這是爲了保證前後版本的兼容性,defaultReadObject會幫我們給對象的非transient字段默認賦值。加入我們升級了版本在第二個版本中新增了某個屬性,而且上一個版本沒有調用默認的defaultReadObject,最新版本的字節流在上一個版本發序列化,那麼反序列化會失敗。

5.5 單例控制的類不要實現序列化接口。

單例控制的類不要實現序列化接口,如果實現了,反序列化的時候實際上就不再是單例了,每次反序列化都會創建一個新的實例。

5.6 考慮使用序列化代理模式

  • 序列化代理實際上就是一個實現了序列化接口的靜態內部類,並且和外部類具有相同的參數,然後在外部類中重寫writeReplace(),通過代理類去做序列化,反序列化的時候使用readResolve方法。(該方法已經不被官方推薦了)
    個人不太看好序列化代理模式,所以也沒有詳細代碼介紹該模式。
    一是寫起來很繁瑣;二是破壞了兼容性,如果將來外部類需要被子類重寫,那麼就不可能和子類兼容。好處是防止僞字節流攻擊和內部敏感域盜用。

參考資料:
java序列化規範 主要關注第六章
序列化協議介紹

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章