什麼是序列化:將對象編碼成一個字節流,這樣一來就能夠在通訊中傳遞對象了。好比在一臺虛擬機中被傳遞到另外一臺虛擬機中,或者字節流存儲到磁盤上。java
「關於Java的序列化,無非就是簡單的實現Serializable接口」這樣的說法只能說明停留在會用的階段,而咱們想要走的更遠每每就須要瞭解更多的東西,好比:爲何要實現序列化?序列化對程序的安全性有啥影響?如何避免多餘的序列化?.....安全
本文主要參考資料《Effective Java》,其中代碼除了只做部分說明,不能運行外,剩餘代碼都是親自實踐過的!ide
雖然實現Serializable很簡單,可是爲了序列化而付出的長期開銷每每是實實在在的。實現Serializable接口而付出的最大代價是,一旦一個類被髮布,就大大下降了「改變這個類的實現」的靈活性。工具
問:這個靈活性具體是指什麼呢?測試
即一旦類實現了Serializable接口,而且這個類被普遍地使用,每每必須永遠支持這種序列化形式,若是使用默認的序列化形式,那麼這種序列化形式將永遠地束縛在該類最初的內部表示法上,換句話說,一旦接受了默認的序列化形式,這個類中私有的和包級私有的實例域都變成導出的API的一部分,這顯然是不符合的。這也就是實現序列化每每須要考慮到的幾個代價,具體請往下看!this
若是沒有顯式聲明序列版本UID,對對象的需求進行了改動,那麼兼容性將會遭到破壞,在運行時致使InvalidClassException。好比:增長一個不是很重要的工具方法,自動產生的序列版本UID也會發生變化,則會出現序列版本UID不一致的狀況。因此最好仍是顯式的增長序列版本號UID。編碼
對User JavaBean實現Serializable接口,增長固定的序列版本號spa
public class User implements Serializable { /** 顯示增長序列版本UUID,自動生成UUID可能會致使InvalidClassException */ private static final long serialVersionUID = 1L; public User(int id, String name) { this.id = id; this.name = name; } private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + '}'; } }
使用ObjectOutputStream與ObjectInputStream流控制序列與反序列3d
/** * @author jian * @date 2019/4/5 * @description 測試序列化 */ public class SeriablizableTest { public static void main(String[] args) { User user = new User(1, "lijian"); serializeUser(user); deserializeUser(); } /** * 使用writeObject方法序列化 * * @param user */ private static void serializeUser(User user) { ObjectOutputStream outputStream = null; try { // 建立對象輸出流, 包裝一個其它類型目標輸出流,如文件流 outputStream = new ObjectOutputStream(new FileOutputStream("D:\\user.txt")); // 經過對象輸出流的writeObject方法將對象user寫入流中 outputStream.writeObject(user); System.out.println("user序列化成功!"); } catch (IOException e) { e.printStackTrace(); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } private static void deserializeUser() { User user = null; Employee employee = null; ObjectInputStream inputStream = null; try { // 建立對象輸出流, 包裝一個其它類型目標輸出流,如文件流 inputStream = new ObjectInputStream(new FileInputStream("D:\\user.txt")); // 經過對象輸出流的writeObject方法將對象user寫入流中 user = (User)inputStream.readObject(); System.out.println("user反序列化成功:" + user); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
輸出結果:先看user.txt文件中二進制文件流(由於txt打不開二進制流,因此是亂碼)code
以後再看控制檯中,反序列化輸出的User{id=1, name='lijian'},說明整個過程序列化成功!
以後去掉固定的序列版本號UID,讓其自動生成,同時增長age屬性(或者手動修改UID爲2L)
private static final long serialVersionUID = 2L;
只進行反序列化將會報錯: java.io.InvalidClassException
public static void main(String[] args) { User user = new User(1, "lijian"); // serializeUser(user); deserializeUser(); }
序列化機制是一種語言以外的對象建立機制,反序列化機制都是一個「隱藏的構造器」,具有與其餘構造器相同的特色,正式由於反序列化中沒有顯式構造器,因此很容易就會忽略:不容許攻擊者訪問正在構造過程當中的對象內部信息。換句話說,序列化後的字節流能夠被截取進行僞造,以後利用readObject方法反序列會不符合要求甚至不安全的實例。
一個可序列化的類被修訂時,須要檢查是否「在新版本中序列化一個實例,能夠在舊版本中反序列化」,若是一個實現序列化的類有不少的子類或者是被修改時,就不得不加以測試。
一、序列化是保存對象的狀態,也就是不會關心static靜態域,靜態域不會被序列化。如User中count靜態域。
public class User implements Serializable { private static final long serialVersionUID = 1L; private static int count = 1; public User(int id, String name) { // 約束條件name不能爲null if (name == null || StringUtils.isEmpty(name)) { throw new NullPointerException("name is null"); } this.id = id; this.name = name; } public User(){}; private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getCount() { return count; } public void setCount(int count) { User.count = count; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", count=" + count + '}'; } private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { inputStream.defaultReadObject(); // 約束條件name不能爲null if (name == null || StringUtils.isEmpty(name)) { throw new NullPointerException("name is null"); } } }
賦值count爲20:
public static void main(String[] args) { User user = new User(); user.setName("Lijian"); user.setId(1); user.setCount(20); serializeUser(user); deserializeUser(); }
序列化-反序列化
/** * 使用writeObject方法序列化 * * @param user */ private static void serializeUser(User user) { ObjectOutputStream outputStream = null; try { // 建立對象輸出流, 包裝一個其它類型目標輸出流,如文件流 outputStream = new ObjectOutputStream(new FileOutputStream("D:\\user.txt")); // 經過對象輸出流的writeObject方法將對象user寫入流中 outputStream.writeObject(user); System.out.println("user序列化成功!"); } catch (IOException e) { e.printStackTrace(); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } private static void deserializeUser() { User user = null; ObjectInputStream inputStream = null; try { // 建立對象輸出流, 包裝一個其它類型目標輸出流,如文件流 inputStream = new ObjectInputStream(new FileInputStream("D:\\user.txt")); // 經過對象輸出流的writeObject方法將對象user寫入流中 user = (User)inputStream.readObject(); // User靜態變量初始化爲0,不會被反序列化 System.out.println("user反序列化成功!"); System.out.println("id:" + user.getId()); System.out.println("name:" + user.getName()); System.out.println("count:" + user.getCount()); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
控制它輸出:count明明被賦值爲20,可是反序列化後輸出爲0,說明static是不會參數序列化的,跟transient相似。最終在反序列化過程當中會被初始化爲默認值(基本數據類型爲0,對象引用爲null,boolean爲false)
二、在序列化對象時,若是該對象中有引用對象域名,那麼也要要求該引用對象是可實例化的。如序列化User實例,其中引用了Employee實例,那麼也須要對Employee進行可序列化操做,不然會報錯: java.io.NotSerializableException
User增長對Employee引用:
/** 對外引用其它對象,若是序列化該實例,則該對象實例也必須能實例化(implement Serializable) */ public Employee employee = new Employee(1, "Java programmer");
Employee不實現序列化:
public class Employee{ private int code; private String position; public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getPosition() { return position; } public void setPosition(String position) { this.position = position; } public Employee(int code, String position) { this.code = code; this.position = position; } @Override public String toString() { return "Employee{" + "code=" + code + ", position='" + position + '\'' + '}'; } }
測試類:
/** * @author jian * @date 2019/4/5 * @description 測試序列化 */ public class SeriablizableTest { public static void main(String[] args) { User user = new User(1, "lijian"); serializeUser(user); deserializeUser(); } /** * 使用writeObject方法序列化 * * @param user */ private static void serializeUser(User user) { ObjectOutputStream outputStream = null; try { // 建立對象輸出流, 包裝一個其它類型目標輸出流,如文件流 outputStream = new ObjectOutputStream(new FileOutputStream("D:\\user.txt")); // 經過對象輸出流的writeObject方法將對象user寫入流中 outputStream.writeObject(user); System.out.println("user序列化成功!"); } catch (NotSerializableException e) { System.out.println("user引用employee對象域序列化失敗"); } catch (IOException e) { e.printStackTrace(); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } private static void deserializeUser() { User user = null; Employee employee = null; int id = 0; ObjectInputStream inputStream = null; try { // 建立對象輸出流, 包裝一個其它類型目標輸出流,如文件流 inputStream = new ObjectInputStream(new FileInputStream("D:\\user.txt")); // 經過對象輸出流的writeObject方法將對象user寫入流中 user = (User)inputStream.readObject(); System.out.println("user引用employee對象域反序列化成功"); System.out.println("user反序列化成功:" + user); } catch (WriteAbortedException e) { System.out.println("user引用employee對象域反序列化失敗"); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
控制檯輸出結果:
要解決這樣的問題,要麼將 Employee implement Serializable ,要麼對Employee對象實例transient修飾: public transient Employee employee = new Employee(1, "Java programmer"); 。可是須要注意的是序列化過程會對transient修飾的域初始化爲默認值(對象引用爲null,基本數據類型爲0,boolean爲false),因此執行以上代碼會出現 java.lang.NullPointerException
三、默認序列化的過程可能消耗大量內存空間和時間,甚至可能會引發棧溢出:由於第二條的緣由,若是一個類中大量存在引用對象域,而且都須要實現序列化,那麼整個序列化過程可能會很消耗時間,在通訊傳輸過程當中更是如此,同時序列化後的字節流須要足夠大的內存。
即便肯定了默認的序列化形式是合適的,一般還必須提供一個readObject方法以保證約束關係和安全性。readObject方法至關於另外一個共有構造器(能夠認爲是用「字節流做爲惟一參數」的構造器),跟其它構造器同樣,它也要求一樣的全部主要事項:構造器必須檢查參數的有效性,必要時對參數進行保護性拷貝等。readObject若是沒有作到,那麼對於攻擊者來講違反這個類的約束條件相對就比較簡單了,若是對一我的工仿造的字節流(人工修改從實例序列後的字節流)時,readObject產生的對象會違反所屬類的約束條件。
1)爲了解決這個問題,User中須要提供了readObject方法,該方法首先調用defalutReadObject,而後檢查被反序列化以後的對象的有效性,若是有效性檢查失敗,readObject方法就會拋出InvalidObjectException異常,使反序列過程不能成功。
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { inputStream.defaultReadObject(); }
User中的構造器中已對參數name約束爲不能爲null
public User(int id, String name) { // 約束條件name不能爲null或空 if (name == null || StringUtils.isEmpty(name)) { throw new NullPointerException("name is null or empty"); } this.id = id; this.name = name; }
2)那麼readObject中也應該對其name進行約束,不然人工僞造的字節流很容易經過readObject構造出沒有任何約束的對象實例,形成安全隱患。
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { inputStream.defaultReadObject(); // 約束條件name不能爲null或空 if (name == null || StringUtils.isEmpty(name)) { throw new NullPointerException("name is null or empty");
}
}
儘管以上兩種修正已經有效地避免攻擊者建立無效的User實例,可是還有一種狀況經過僞造字節流能夠建立可變的User實例:好比User中增長Date對象引用birthday私有域,而後經過附加僞造字節流指向該birthday引用,攻擊者從ObjectInputStream中讀取User實例,而後讀取附加後面的惡意Date引用,經過該Date引用就能夠可以訪問User對象內部私有Date域所引用的對象,從而改變User實例。
代碼以下:
public class MutableUser { public User user; public Date birthday; public MutableUser(){ try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos); // 字節流有效的User實例開頭,而後附加額外的引用 out.writeObject(new User(new Date())); // 假設這是惡意的二進制,即附加惡意對象引用Date byte[] ref = {0x71, 0, 0x7e, 0 ,5}; bos.write(ref); // 攻擊者從ObjectInputStream中讀取User實例,而後讀取附加在後面的「惡意編制對象引用Date」 ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); user = (User) in.readObject(); birthday = (Date) in.readObject(); } catch (Exception e) { } } public static void main(String[] args) { MutableUser mutableUser = new MutableUser(); User user = mutableUser.user; Date birthday = mutableUser.birthday; // 攻擊者修改User內部birthday私有域,年份更改成2018 birthday.setTime(2018); System.out.println(user); } }
注:以上代碼運行不了,只會加以解釋說明而已,具體能夠查看《Effective Java》中的代碼舉例
爲了解決此問題,提出第三個安全措施
3)當一個對象被反序列化時,客戶端不該該擁有對象的引用,若是哪一個域包含了這樣的對象引用,若是包含了私有的域(組件),就必需要保護性拷貝(非final域):當User對象在客戶端MutableUser反序列化時,客戶端擁有 了不應擁有的User私有域Date引用birthday,因此應該在readObject對birthday進行拷貝:
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { inputStream.defaultReadObject(); // 保護性拷貝birthday birthday = new Date(birthday.getTime()); // 約束條件name不能爲null if (name == null || StringUtils.isEmpty(name)) { throw new NullPointerException("name is null"); } }
1)使用readObject其實就跟正常無參數的構造器同樣,該知足的約束須要知足,同時必要時進行保護性拷貝。
2)反序列化過程最終會調用readObject方法,以下是一個異常棧的調用關係(代碼中故意讓readObject方法拋異常):deserialize---->ObjectInputStream.readObject----->ObjectInputStream.readObject0----->......User.readObject
可是若是Sinleton類實現了序列化,那麼它再也不是一個Singleton,不管該類使用了默認的序列化形式,仍是自定義的序列化形式,仍是是否提供顯式的readObject方法都不要緊。任何一個readObject方法,不論是顯式仍是默認的,它都會返回一個新建的實例,這個新建的實例不一樣於該類初始化時建立的實例。
簡單的Singleton:
public class Singleton { private static Singleton INSTANCE= new Singleton(); private Singleton(){}; ..... }
readResolve特性容許使用readObject建立實例代替另外一個實例,若是一個類定義了readResolve方法,而且具有正確的聲明,那麼在反序列化的以後,新建的readResolve方法就會被調用,而後返回的對象引用將被返回,取代新建的對象。
public class Singleton implements Serializable { private static Singleton INSTANCE= new Singleton(); private Singleton(){}; private Object readResolve(){ return INSTANCE; } }