Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java
條目 50 裏有一個不可變的日期範圍類,它包含一個可變的私有Date屬性。 該類經過在其構造方法和訪問器中防護性地拷貝Date對象,不遺餘力維持其不變性(invariants and immutability)。 代碼以下所示:git
// Immutable class that uses defensive copying public final class Period { private final Date start; private final Date end; /** * @param start the beginning of the period * @param end the end of the period; must not precede start * @throws IllegalArgumentException if start is after end * @throws NullPointerException if start or end is null */ public Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (this.start.compareTo(this.end) > 0) throw new IllegalArgumentException( start + " after " + end); } public Date start () { return new Date(start.getTime()); } public Date end () { return new Date(end.getTime()); } public String toString() { return start + " - " + end; } ... // Remainder omitted }
假設要把這個類可序列化。因爲Period
對象的物理表示精確地反映了它的邏輯數據內容,因此使用默認的序列化形式是合理的(條目 87)。所以,要使類可序列化,彷佛只需將implements Serializable 添加到類聲明中就能夠了。可是,若是這樣作,該類再也不保證它的關鍵不變性了。github
問題是readObject方法其實是另外一個公共構造方法,它須要與任何其餘構造方法同樣的當心警戒。 正如構造方法必須檢查其參數的有效性(條目 49)並在適當的地方對參數防護性拷貝(條目 50),readObject方法也要這樣作。 若是readObject方法沒法執行這兩個操做中的任何一個,則攻擊者違反類的不變性是相對簡單的事情。數組
簡而言之,readObject是一個構造方法,它將字節流做爲惟一參數。 在正常使用中,字節流是經過序列化正常構造的實例生成的。當readObject展示一個字節流時,問題就出現了,這個字節流是人爲構造的,用來生成一個違反類不變性的對象。 這樣的字節流可用於建立一個不可能的對象,該對象沒法使用普通構造方法建立。安全
假設咱們只是將implements Serializablet
添加到Period
類聲明中。 而後,這個醜陋的程序生成一個Period實例,其結束時間在其開始時間以前。 對byte類型的值進行強制轉換,其高階位被設置,這是因爲Java缺少byte字面量,而且錯誤地決定對byte類型進行簽名:測試
public class BogusPeriod { // Byte stream couldn't have come from a real Period instance! private static final byte[] serializedForm = { (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8, 0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02, 0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f, 0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75, 0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a, (byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00, 0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf, 0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03, 0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22, 0x00, 0x78 }; public static void main(String[] args) { Period p = (Period) deserialize(serializedForm); System.out.println(p); } // Returns the object with the specified serialized form static Object deserialize(byte[] sf) { try { return new ObjectInputStream( new ByteArrayInputStream(sf)).readObject(); } catch (IOException | ClassNotFoundException e) { throw new IllegalArgumentException(e); } } }
用於初始化serializedForm的字節數組字面量(literal)是經過序列化正常的Period實例,並手動編輯生成的字節流生成的。 流的細節對於該示例並不重要,可是若是好奇,則在《Java Object Serialization Specification》[序列化,6]中描述了序列化字節流格式。 若是運行此程序,它會打印Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984
。只需聲明Period
類爲可序列化,咱們就能夠建立一個違反其類不變性的對象。this
要解決此問題,請爲Period提供一個readObject方法,該方法調用defaultReadObject,而後檢查反序列化對象的有效性。若是有效性檢查失敗,readObject方法拋出InvalidObjectException異常,阻止反序列化完成:代理
// readObject method with validity checking - insufficient! private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); // Check that our invariants are satisfied if (start.compareTo(end) > 0) throw new InvalidObjectException(start +" after "+ end); }
雖然這樣能夠防止攻擊者建立無效的Period實例,但仍然存在潛在的更微妙的問題。 能夠經過構造以有效Period實例開頭的字節流來建立可變Period實例,而後將額外引用附加到Period實例內部的私有Date屬性。 攻擊者從ObjectInputStream中讀取Period實例,而後讀取附加到流的「惡意對象引用」。 這些引用使攻擊者能夠訪問Period對象中私有Date屬性引用的對象。 經過改變這些Date實例,攻擊者能夠改變Period實例。 如下類演示了這種攻擊:code
public class MutablePeriod { // A period instance public final Period period; // period's start field, to which we shouldn't have access public final Date start; // period's end field, to which we shouldn't have access public final Date end; public MutablePeriod() { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos); // Serialize a valid Period instance out.writeObject(new Period(new Date(), new Date())); /* * Append rogue "previous object refs" for internal * Date fields in Period. For details, see "Java * Object Serialization Specification," Section 6.4. */ byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5 bos.write(ref); // The start field ref[4] = 4; // Ref # 4 bos.write(ref); // The end field // Deserialize Period and "stolen" Date references ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream(bos.toByteArray())); period = (Period) in.readObject(); start = (Date) in.readObject(); end = (Date) in.readObject(); } catch (IOException | ClassNotFoundException e) { throw new AssertionError(e); } } }
要查看正在進行的攻擊,請運行如下程序:component
public static void main(String[] args) { MutablePeriod mp = new MutablePeriod(); Period p = mp.period; Date pEnd = mp.end; // Let's turn back the clock pEnd.setYear(78); System.out.println(p); // Bring back the 60s! pEnd.setYear(69); System.out.println(p); }
在個人語言環境中,運行此程序會產生如下輸出:
Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1978 Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969
雖然建立了Period實例且保持了其不變性,但能夠隨意修改其內部組件。 一旦擁有可變的Period實例,攻擊者可能會經過將實例傳遞給依賴於Period的安全性不變性的類來形成巨大的傷害。 這並不是如此牽強:有些類就是依賴於String的不變性來保證安全性的。
問題的根源是Period類的readObject方法沒有作足夠的防護性拷貝。 對象反序列化時,防護性地拷貝包含客戶端不能擁有的對象引用的屬性,是相當重要的。 所以,每一個包含私有可變組件的可序列化不可變類,必須在其readObject方法中防護性地拷貝這些組件。 如下readObject方法足以確保Period的不變性並保持其不變性:
// readObject method with defensive copying and validity checking private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); // Defensively copy our mutable components start = new Date(start.getTime()); end = new Date(end.getTime()); // Check that our invariants are satisfied if (start.compareTo(end) > 0) throw new InvalidObjectException(start +" after "+ end); }
請注意,防護性拷貝在有效性檢查以前執行,而且咱們沒有使用Date的clone方法來執行防護性拷貝。 須要這兩個細節來保護Period免受攻擊(條目 50)。 另請注意,final屬性沒法進行防護性拷貝。 要使用readObject方法,咱們必須使start和end屬性不能是final類型的。 這是不幸的,但它是這兩個中較好的一個作法。 使用新的readObject方法並從start
和end
屬性中刪除final修飾符後,MutablePeriod
類再也不無效。 上面的攻擊程序如今生成以下輸出:
Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017 Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
下面是一個簡單的石蕊測試(litmus test),用於肯定類的默認readObject方法是否可接受:你是否願意添加一個公共構造方法,該構造方法把對象中每一個非瞬時狀態的屬性值做爲參數,並在沒有任何驗證的狀況下,將值保存在屬性中?若是沒有,則必須提供readObject方法,而且它必須執行構造方法所需的全部有效性檢查和防護性拷貝。或者,可使用序列化代理模式(serialization proxy pattern))(條目 90)。強烈推薦使用這種模式,由於它在安全反序列化方面花費了大量精力。
readObject方法和構造方法還有一個類似之處,它們適用於非final可序列化類。 與構造方法同樣,readObject方法不能直接或間接調用可重寫的方法(條目 19)。 若是違反此規則而且重寫了相關方法,則重寫方法會在子類狀態被反序列化以前運行。 程序可能會致使失敗[Bloch05,Puzzle 91]。
總而言之,不管什麼時候編寫readObject方法,都要採用這樣一種思惟方式,即正在編寫一個公共構造方法,該構造方法必須生成一個有效的實例,而無論給定的是什麼字節流。不要假設字節流必定表示實際的序列化實例。雖然本條目中的示例涉及使用默認序列化形式的類,可是所引起的全部問題都一樣適用於具備自定義序列化形式的類。下面是編寫readObject方法的指導原則:
對於具備必須保持私有的對象引用屬性的類,防護性地拷貝該屬性中的每一個對象。不可變類的可變組件屬於這一類別。
檢查任何不變性,若是檢查失敗,則拋出InvalidObjectException異常。 檢查應再任何防護性拷貝以後。
若是必須在反序列化後驗證整個對象圖(object graph),那麼使用ObjectInputValidation接口(在本書中沒有討論)。
不要直接或間接調用類中任何可重寫的方法。