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 中實現安全的使用者身份驗證系統的方式!在上面的資訊中,有一些點被一筆帶過,也有一些點完全被省略了(例如,如何只在必要時啟動會話)。如果你正在實現自己的使用者登入系統並嘗試遵循這裡給出的建議,你肯定會遇到一些在進行過程中會發現並希望在這裡解釋的事情,因此請大膽地編輯此頁面,新增你認為缺失的任何內容。
|
|
如果你認為需要更改某些內容,請不要猶豫!華夏公益教科書是一個維基,這意味著每個人(包括你)都可以透過點選編輯此頁面來編輯任何頁面。你甚至不需要登入!華夏公益教科書總是歡迎新手,所以不要害怕犯錯。如果你不知道如何編輯頁面,請檢視幫助:編輯或在沙盒中嘗試。 |
