- 原文地址:The Art of Defensive Programming
- 原文作者:Diego Mariani
- 譯文出自:掘金翻譯計劃
- 譯者:GiggleAll
- 校對者:tanglie1993 , fghpdf
防守式編程的藝術
爲什麼開發人員不編寫安全代碼? 我們不再在這裏討論 “乾淨的代碼” 。我們從一個純粹的角度,軟件的安全性來討論更多的東西。是的,因爲一個不安全的軟件幾乎是沒用的。讓我們來看看不安全的軟件意味着什麼。
歐洲航天局的 Ariane 5 Flight 501 在起飛後 40 秒(1996年6月4日)被毀。10 億美元的原型火箭由於機載導航軟件中的錯誤而自毀。
在 20 世紀 80 年代,一個治療機中控制 Therac-25 輻射的的代碼錯誤,導致其施用過量的 X 射線致使至少五名患者死亡。
MIM-104 愛國者的軟件錯誤導致其系統時鐘在 100 小時時段內偏移三分之一秒,以至於無法定位和攔截來襲導彈。伊拉克導彈襲擊了沙特阿拉伯在達哈蘭的一個軍事大院( 1991 年 2 月 25 日 ),殺害了 28 名美國人。
這些例子足以讓我們認識到編寫安全的軟件,特別是在某些情況下是多麼重要。在其他使用情況下,我們也應該知道我們軟件錯誤會帶給我們什麼。
防守式編程角度一
爲什麼我認爲防守式編程在某些項目中是一個發現這些問題的好方法?
防禦不可能,因爲不可能將可能發生。
對於防禦性編程有很多定義,它還取決於安全性的級別和您的軟件項目所需的資源級別。
防守式編程是一種防守式設計,旨在確保在意外的情況下軟件的持續性功能,防守式編程實踐常被用在高可用性,需要安全的地方 — 維基百科
我個人認爲這種方法適合當你處理一個大的、長期的、有許多人蔘與的項目。 例如,需要大量維護的開源項目。
爲了實現防守式編程方法,讓我談談我個人簡陋的觀點。
從不相信用戶輸入
假設你總是會收到你意料之外的東西。這應該是你作爲防守式程序員的方法,針對用戶輸入,或者平常進入你的系統的各種東西。因爲我們可以預料到意想不到的,儘量做到儘可能嚴格。斷言你的輸入值是你期望的。
進攻就是最好的防守
(將輸入)列入白名單而不是把它放到黑名單中,例如,當驗證圖像擴展名時,不檢查無效的類型,而是檢查有效的類型,排除所有其餘的類型。 在 PHP 中,也有無數的開源驗證庫來使你的工作更容易。
進攻就是最好的防守,控制要嚴格。
使用數據抽象
OWASP 十大安全漏洞 中的第一個是注入。這意味着有人(很多人)還沒有使用安全工具來查詢他們的數據庫。請使用數據庫抽象包和庫。在 PHP 中你可以使用 PDO 來確保基本的注入保護。
不要重複造輪子
你不用框架(或微框架)? 你就是喜歡沒有理由的做額外的工作。恭喜你!只要是經過良好測試、廣受信任的穩定的代碼,你就可以儘管用於各種新特性(不僅是框架)的開發,而不是隻因爲它是已經造好的輪子的緣故而重新造輪子。你自己造輪子的唯一原因是你需要一些不存在或存在但不適合你的需求(性能不佳,缺少的功能等)。
那個(使用框架)我們稱它爲智能代碼重用,它值得擁有。
不要信任開發人員
防守式編程可以與稱爲防禦性駕駛的東西相關。在防禦駕駛中,我們假設我們周圍的每個人都有可能犯錯誤。 所以我們必須小心別人的行爲。這些同樣適用於我們的防守式編程,作爲開發者,我們不應該相信其他開發者。我們也同樣不應該信任我們的代碼。
在許多人蔘與的大項目中,我們可以有許多不同的方式來編寫和組織代碼。 這也可能導致混亂,甚至更多的錯誤。 這就是爲什麼我們統一編碼風格和使用代碼檢測器會使我們的生活更加輕鬆。
寫SOLID代碼
這是對一個防守式程序員困難的地方,writing code that doesn’t suck。這是許多人知道和談論的事情,但沒有人真正關心或投入正確的注意力和努力來實現 SOLID代碼。
讓我們來看一些不好的例子。
不要:未初始化的屬性
<?php
class BankAccount
{
protected $currency = null;
public function setCurrency($currency) { ... }
public function payTo(Account $to,$amount)
{
// sorry for this silly example
$this->transaction->process($to,$amount,$this->currency);
}
}
// I forgot to call $bankAccount->setCurrency('GBP');
$bankAccount->payTo($joe,100);
在這種情況下,我們必須記住,爲了發出付款,我們需要先調用 setCurrency 。 這是一個非常糟糕的事情,像這樣的狀態更改操作(發出付款)不應該在兩個步驟使用兩個(或多個)公共方法。 我們仍然可以有很多方法來付款,但是我們必須只有一個簡單的公共方法,以改變狀態(對象應該永遠不會處於不一致的狀態)。
在這種情況下,我們可以做得更好,將未初始化的屬性封裝到 Money 對象中。
<?php
class BankAccount
{
public function payTo(Account$to,Money$money){ ... }
}
$bankAccount->payTo($joe,newMoney(100,newCurrency('GBP')));
使它萬無一失。 不要使用未初始化的對象屬性。
Don’t: Leaking state outside class scope.
不要:類作用域之外的暴露狀態。
<?php
class Message
{
protected $content;
public function setContent($content)
{
$this->content=$content;
}
}
class Mailer
{
protected $message;
public function__construct(Message$message)
{
$this->message=$message;
}
public function sendMessage(
{
var_dump($this->message);
}
}
$message = new Message();
$message->setContent("bob message");
$joeMailer = new Mailer($message);
$message->setContent("joe message");
$bobMailer = new Mailer($message);
$joeMailer->sendMessage();
$bobMailer->sendMessage();
在這種情況下,消息通過引用傳遞,結果將在兩種情況下都是 “joe message” 。 解決方案是在 Mailer 構造函數中克隆消息對象。 但是我們應該總是嘗試使用一個(不可變的)值對象去替代一個簡單的 Message mutable對象。當你可以的時候使用不可變對象。
<?php
class Message
{
protected $content;
public function __construct($content)
{
$this->content = $content;
}
}
class Mailer
{
protected $message;
public function __construct(Message $message)
{
$this->message = $message;
}
public function sendMessage()
{
var_dump($this->message);
}
}
$joeMailer = new Mailer(new Message("bob message"));
$bobMailer = new Mailer(new Message("joe message"));
$joeMailer->sendMessage();
$bobMailer->sendMessage();
寫測試
我們還需要說些什麼? 寫單元測試將幫助您遵守共同的原則,如高聚合,單一責任,低耦合和正確的對象組合。 它不僅幫助你測試小單元,而且也能測試你的對象的結構的方式。 事實上,你會清楚地看到,爲了測試你的小功能需要測試多少個單元和你需要模擬多少個對象,以實現100%的代碼覆蓋率。
總結
希望你喜歡這篇文章。 記住這些只是建議,何時、何地採納這些建議,這取決於你。