??Java是一種安全的語言添诉,這是它的一大優(yōu)點挑社。這意味著在沒有本機方法的情況下,它不受緩沖區(qū)溢出顿痪、數(shù)組溢出镊辕、野生指針和其他內存損壞錯誤的影響油够,這些錯誤困擾著C和c++等不安全語言。在一種安全的語言中丑蛤,可以編寫類并確定它們的不變量將保持不變吁系,不管在系統(tǒng)的任何其他部分發(fā)生了什么汉矿。在將所有內存視為一個巨大數(shù)組的語言中,這是不可能的动猬。
??即使使用一種安全的語言虏束,如果您不付出一些努力棉饶,也無法與其他類隔離。您必須進行防御性的編程镇匀,并假定您的類的客戶端會盡最大努力破壞它的不變量照藻。隨著人們更加努力地破壞系統(tǒng)的安全性,這一點越來越正確汗侵,但更常見的情況是幸缕,您的類將不得不處理由善意程序員的誠實錯誤所導致的意外行為。無論哪種方式晰韵,都值得花時間編寫面對行為不端的客戶端的健壯類发乔。
??雖然如果沒有對象的幫助,另一個類是不可能修改對象的內部狀態(tài)的雪猪,但是提供這樣的幫助卻出奇地容易栏尚。例如,考慮下面的類只恨,它聲稱表示一個不可變的時間段:
??乍一看译仗,這個類似乎是不可變的,并且強制一個周期的開始不跟隨它的結束官觅。然而纵菌,利用日期是可變的這一事實很容易違反這個不變量:
??從Java 8開始,解決這個問題的明顯方法就是使用Instant(或LocalDateTime或ZonedDateTime)代替Date休涤,因為Instant(和其他Java.time 類)是不可變的( item17 )咱圆。Date已過時,不應在新代碼中使用滑绒。盡管如此闷堡,問題仍然存在:有時您必須在api和內部表示中使用可變值類型,本項目中討論的技術適用于這些時候疑故。
??為了保護Period實例的內部不受這種攻擊杠览,必須將每個可變參數(shù)的防御性副本復制到構造函數(shù)并使用副本代替正本,作為期間實例的組成部分:
?? 有了新的構造函數(shù)纵势,之前的攻擊將不會對Period實例產生影響踱阿。注意到防御副本在檢查參數(shù)有效性(item49)前制作管钳,對副本而不是原件進行有效性檢查。雖然這看起來不自然软舌,但卻是必要的才漆。它保護類不受其他線程參數(shù)更改的影響在漏洞窗口期間,從檢查參數(shù)到復制參數(shù)的時間間隔佛点。在計算機安全社區(qū)醇滥,這被稱為檢查時間/使用時間或TOCTOU攻擊[Viega01].
??還要注意,我們沒有使用Date的clone方法來創(chuàng)建防御性副本超营。因為Date是非final的鸳玩,所以不能保證克隆方法返回一個類為java.util.Date的對象:它可以返回一個不受信任子類的實例,這個子類是專門為惡意破壞而設計的演闭。例如不跟,這樣的子類可以在創(chuàng)建時在私有靜態(tài)列表中記錄對每個實例的引用,并允許攻擊者訪問這個列表米碰。這將使攻擊者可以自由控制所有實例窝革。為了防止這種攻擊,不要使用克隆方法對類型可由不受信任方子類化的參數(shù)進行防御性復制吕座。
??雖然替換構造函數(shù)成功地防御了之前的攻擊虐译,但是仍然可以修改Period實例,因為它的訪問器提供了對其可變內部結構的訪問:
??要防御第二次攻擊米诉,只需修改訪問器菱蔬,返回可變內部字段的防御副本:
??有了新的構造函數(shù)和新的訪問器,Period實際上是不可變的史侣。無論程序員多么惡意或無能拴泌,都不可能違背period 的開始和結束不一致這一不變式(不借助語言以外的手段,如native方法和反射).這是真的惊橱,因為除了Period本身之外蚪腐,任何類都無法訪問Period實例中的任何可變字段。這些字段真正封裝在對象中税朴。
??在訪問器中回季,與構造函數(shù)不同,可以使用clone方法進行防御性復制正林。這是因為我們知道Period的內部Date對象的類是java.util.Date泡一,而不是某個不可信的子類。也就是說觅廓,出于第13項中列出的原因鼻忠,通常最好使用構造函數(shù)或靜態(tài)工廠來復制實例。
??參數(shù)的防御性復制不僅適用于不可變類杈绸。在編寫方法或構造函數(shù)時帖蔓,如果要在內部數(shù)據(jù)結構中存儲對客戶機提供的對象的引用矮瘟,請考慮客戶機提供的對象是否可能是可變的。如果是塑娇,請考慮在對象進入數(shù)據(jù)結構之后澈侠,您的類是否能夠容忍對象中的更改。如果答案是否定的埋酬,則必須防御性地復制對象哨啃,并將副本輸入到數(shù)據(jù)結構中,而不是原始結構中奇瘦。舉個例子,如果你正在考慮使用一個對象引用作為一個具備此元素在內部設置實例或作為一個關鍵的內部地圖實例,您應該意識到的不變量設置或地圖會損壞如果對象被修改后插入棘催。
??在將內部組件返回給客戶端之前對其進行防御性復制也是如此劲弦。無論您的類是否是不可變的耳标,在返回對可變內部組件的引用之前,您都應該三思邑跪。很有可能次坡,您應該返回一個防御性副本。記住画畅,非零長度數(shù)組總是可變的砸琅。:因此,在將內部數(shù)組返回給客戶機之前轴踱,應該始終創(chuàng)建一個防御性的副本症脂。或者淫僻,您可以返回數(shù)組的不可變視圖诱篷。這兩種技術都顯示在項目15中。
??可以說雳灵,所有這些的真正教訓是棕所,在可能的情況下,應該使用不可變對象作為對象的組件悯辙,這樣就不必擔心防御性復制(item17)琳省。在我們的Period示例中,使用Instant(或LocalDateTime或ZonedDateTime)躲撰,除非您使用的是Java 8之前的版本针贬。如果使用較早的版本,一個選項是存儲Date. gettime()返回的long原語拢蛋,而不是Date引用桦他。
??可以說,所有這些的真正教訓是瓤狐,在可能的情況下瞬铸,應該使用不可變對象作為對象的組件批幌,這樣就不必擔心防御性復制(item17)。在我們的Period示例中嗓节,使用Instant(或LocalDateTime或ZonedDateTime)荧缘,除非您使用的是Java 8之前的版本。如果使用較早的版本拦宣,一個選項是存儲Date. gettime()返回的long原語截粗,而不是Date引用。如果一個類信任它的調用者不修改內部組件鸵隧,可能是因為類和它的客戶端都是同一個包的一部分绸罗,那么就應該避免防御性復制。在這種情況下豆瘫,類文檔應該表明調用者不能修改受影響的參數(shù)或返回值珊蟀。
??即使跨越包邊界,在將可變參數(shù)集成到對象之前對其進行防御性復制也并不總是合適的外驱。有一些方法和構造函數(shù)育灸,它們的調用指示參數(shù)引用的對象的顯式切換。當調用這樣一個方法時昵宇,客戶端承諾不再直接修改對象磅崭。希望擁有客戶機提供的可變對象所有權的方法或構造函數(shù)必須在其文檔中明確說明這一點。
??包含方法或構造函數(shù)的類瓦哎,這些方法或構造函數(shù)的調用指示控制權的轉移砸喻,不能保護自己免受惡意客戶機的攻擊。只有當一個類和它的客戶機之間存在相互信任蒋譬,或者對類的不變量的破壞只會對客戶機造成傷害時割岛,這樣的類才是可接受的。后一種情況的一個例子是包裝器類模式(item18 )羡铲。根據(jù)包裝器類的性質蜂桶,客戶機可以在包裝對象之后直接訪問對象,從而破壞類的不變量也切,但這通常只會損害客戶機扑媚。
??總而言之,如果一個類具有從客戶端獲取或返回給客戶端的可變組件雷恃,則該類必須防御性地復制這些組件疆股。如果復制的成本過高,并且類信任它的客戶端不會不適當?shù)匦薷慕M件倒槐,那么防御性的復制可能被概述客戶端不修改受影響組件的責任的文檔所取代旬痹。
本文寫于2019.7.17,歷時1天