PHP 程式設計/構建安全的使用者登入系統
許多初學者 PHP 程式設計師開始構建具有使用者登入系統的網站,但他們沒有意識到等待的陷阱。以下是對使用者身份驗證系統和使用者授權系統的必要元件的逐步指南。前者是關於確定使用者是否是他們所說的那樣,而後者則關注使用者是否被允許做他們試圖做的事情(例如,訪問特定頁面或執行特定查詢)。
建議讀者將下面概述的概念整合到他們的指令碼中,而不僅僅是複製貼上程式碼示例。登入系統的安全性取決於對開發軟體的個人的信任。由於任何人都可以編輯這些頁面,包括那些懷有惡意意圖的人,因此你不應該信任程式碼示例。 |
身份驗證過程包含兩個部分
- 登入表單。使用者被提供了一些輸入其憑據的方式;系統會將這些憑據與已知使用者列表進行比對;如果找到匹配項,則使用者將被驗證。系統的一部分通常還會啟動某種方式來記住使用者已透過身份驗證(例如透過設定 cookie),這樣使用者不必在每次請求時都重複此過程。
- 每次請求檢查(為了更好的名稱!)。這與登入表單過程的第二部分相同,使用者憑據是從對使用者更方便的來源獲取的,例如 cookie。
下面給出的程式碼可能需要根據你的指令碼架構進行調整,無論是面向物件還是過程式,是隻有一個入口點還是十幾個單獨呼叫的指令碼。但是,登入系統的元件如何執行,它們的基本原理是相同的。類似地,當提到“資料庫”時,它並不一定意味著 MySQL 或任何其他 RDBMS;使用者資訊可以儲存在平面檔案中、LDAP 伺服器上或以其他方式儲存。
這是系統中最簡單的一部分,也是最容易開始的地方。簡而言之:向用戶展示一個 HTML 表單,使用者輸入其憑據,表單內容被提交到登入系統的下一部分進行處理。
使用者的憑據通常是使用者名稱和密碼,儘管也可能使用其他憑據(例如來自硬體令牌生成器的隨機數)。許多網站現在使用使用者的電子郵件地址而不是使用者名稱。這樣做的好處是,電子郵件地址對於每個使用者都是唯一的,而且允許人們擁有一致的使用者名稱,因為具有常見使用者名稱的使用者可能無法在所有註冊的地方獲得相同的使用者名稱。
<form action="/login" method="post">
<p>
<label for="email">Email address:</label>
<input id="email" type="text" name="email" />
</p>
<p>
<label for="password">Password:</label>
<input id="password" type="password" name="password" />
</p>
<p>
<input type="submit" name="login" value="Login" />
</p>
</form>
來自登入表單的資料可以提交到處理身份驗證和登入過程的任何指令碼,並且在這個指令碼中,真正的登入操作發生。
在登入時,我們不必像建立或更改帳戶時那樣擔心檢查輸入,因為我們只是將輸入的內容與資料庫中的內容進行匹配。任何非法的輸入(例如沒有@符號的電子郵件地址)都將簡單地無法匹配,登入將失敗。所有使用者提交的資料在傳遞到資料庫時都會被安全地轉義,因此我們目前不必擔心SQL 注入攻擊,並且從限制使用者名稱或密碼的長度或形式方面,沒有任何安全優勢可言。
登入處理指令碼本身需要做一些事情。首先,它啟動一個會話(這通常實際上是“每次請求身份驗證”的一部分,如下所述)。其次,它查詢資料庫以查詢匹配的使用者;如果不存在,則登入嘗試失敗,使用者將被返回到登入表單。最後(假設使用者確實存在),他們的識別符號(使用者名稱或電子郵件地址)將被儲存為會話變數。
session_start();
$db = new Database(); // Database abstraction class.
$email_address = $db->esc($_POST['email']);
$password = $db->esc($_POST['password']);
$matching_users = $db->get_num_rows("SELECT 1 FROM `users` WHERE email_address='$email_address' AND password=crypt('$password', password) LIMIT 1");
if ($matching_users) {
// User exists; log user in.
$_SESSION['email_address'] = $email_address;
echo "You are now logged in.";
} else {
// Login failed; re-display login form.
}
Database資料庫抽象類用於隱藏資料庫實現細節,以用作此示例的目的,不應被認為代表任何實際存在的庫類。- 其中
Database類的get_num_rows($sql)方法返回$sql查詢產生的行數。上面的LIMIT 1意味著這將只返回 0 或 1。
現在指令碼可以顯示歡迎訊息,並且身份驗證將與使用者保持關聯,因此使用者不必在每次載入頁面時都登入。現在讓我們來看看如何做到這一點。
必須在每次 HTTP 請求(即對於頁面、影像或任何其他內容)時驗證使用者的真實性。這表面上與檢視相關會話變數是否已設定一樣簡單
session_start();
if (!isset($_SESSION['email_address']))
{
// User is not logged in, so send user away.
header("Location:/login");
die();
}
// User is logged in; private code goes here.
這在許多方面都足夠了,但它容易受到許多攻擊。它完全依賴於會話與正確使用者繫結。這不是一件好事,因為會話可能被劫持(會話金鑰可能被第三方竊取)或被固定(第三方可以強迫使用者使用第三方知道的會話金鑰)。閱讀本華夏公益教科書的會話頁面,以獲取有關此問題的更多資訊以及如何避免它。
$timeout = 60 * 30; // In seconds, i.e. 30 minutes.
$fingerprint = hash_hmac('sha256', $_SERVER['HTTP_USER_AGENT'], hash('sha256', $_SERVER['REMOTE_ADDR'], true));
session_start();
if ( (isset($_SESSION['last_active']) && $_SESSION['last_active']<(time()-$timeout))
|| (isset($_SESSION['fingerprint']) && $_SESSION['fingerprint']!=$fingerprint)
|| isset($_GET['logout'])
)
{
setcookie(session_name(), '', time()-3600, '/');
session_destroy();
}
session_regenerate_id();
$_SESSION['last_active'] = time();
$_SESSION['fingerprint'] = $fingerprint;
// User authenticated at this point (i.e. $_SESSION['email_address'] can be trusted).
如果要使用$_SESSION['last_active'] 和$_SESSION['fingerprint'] 變數,它們還需要在初始登入點(處理登入表單的地方)設定;只需將以下幾行插入設定email_address 變數的地方的上方
$fingerprint = hash_hmac('sha256', $_SERVER['HTTP_USER_AGENT'], hash('sha256', $_SERVER['REMOTE_ADDR'], true));
$_SESSION['last_active'] = time();
$_SESSION['fingerprint'] = $fingerprint;
但是,在使用瀏覽器指紋時要記住一件事,儘管它們確實為你的應用程式增加了一些安全性,但它們並非萬能藥。許多ISP 提供動態 IP 地址,這些地址是定期更改的 IP 地址。如果在使用者瀏覽你的頁面時發生這種情況,他將被踢出他的帳戶。此外,檢查瀏覽器是否相同的程式碼段可以透過修改請求頁面時頭的 Firefox 擴充套件進行修改。
這就是在 PHP5 中實現安全使用者身份驗證系統的方式!上面資訊中有一些要點被一筆帶過,還有一些要點完全被省略了(例如,如何僅在必要時啟動會話)。如果你正在實現自己的使用者登入系統,並且試圖遵循此處給出的建議,那麼在進行過程中,你一定會發現一些事情,並且希望這裡能解釋清楚,因此請大膽地編輯此頁面以新增你認為缺少的任何內容。
|
|
如果你認為需要更改某些內容,請不要猶豫!華夏公益教科書是一個維基,這意味著每個人(包括你)都可以透過點選編輯此頁面來編輯任何頁面。你甚至不需要登入!新來者總是歡迎來到華夏公益教科書,所以不要害怕犯錯。如果你不知道如何編輯頁面,請檢視幫助:編輯或在沙盒中進行實驗。 |
