HTTP 身份驗證 (HTTP authentication) 是一種十分常用而容易實作的驗證方法,它倚賴網頁伺服器的內置功能,大量縮短所需編寫的程式碼,對於用戶驗證的要求不高的系統,是一個很實用的驗證方法。Evert Pot 在他的網誌上討論了如何用 PHP 實作這種用戶驗證。
基本驗證 (Basic Auth)
HTTP 身份驗證有兩個主要的驗證方案:「基本驗證 (basic authentication)」和「摘要驗證 (digest authentication)」,其中基本驗證比較容易實作,所以也比較常見,以下是一個以 PHP 實作的基本驗證:
| HTML | | copy code | | ? |
| 01 | <?php |
| 02 | |
| 03 | $username = null; |
| 04 | $password = null; |
| 05 | |
| 06 | // 若果有 mod_php |
| 07 | if (isset($_SERVER['PHP_AUTH_USER'])) { |
| 08 | $username = $_SERVER['PHP_AUTH_USER']; |
| 09 | $password = $_SERVER['PHP_AUTH_PW']; |
| 10 | |
| 11 | // 其他伺服器 |
| 12 | } elseif (isset($_SERVER['HTTP_AUTHENTICATION'])) { |
| 13 | if (strpos(strtolower($_SERVER['HTTP_AUTHENTICATION']),'basic')===0) |
| 14 | list($username,$password) = explode(':',base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6))); |
| 15 | } |
| 16 | |
| 17 | if (is_null($username)) { |
| 18 | header('WWW−Authenticate: Basic realm="My Realm"'); |
| 19 | header('HTTP/1.0 401 Unauthorized'); |
| 20 | echo 'Text to send if user hits Cancel button'; |
| 21 | die(); |
| 22 | } else { |
| 23 | echo "<p>Hello {$username}.</p>"; |
| 24 | echo "<p>You entered {$password} as your password.</p>"; |
| 25 | } |
| 26 | |
| 27 | ?> |
這不算很複雜,是嗎?請注意用戶名稱和密碼都是用 base64 編碼後傳送到伺服器,除非你的伺服器使用 SSL,否則這不算十分安全。
摘要驗證 (Digest Auth)
摘要驗證旨在使驗證的過程更安全,驗證的過程中密碼永遠不會以明碼方式傳送,它以一個散列的形式被送到伺服器,散列的好處是它不能被還原為明碼,我 們只能以相同的散列函式來計算儲存在伺服器上的密碼,透過比較兩個散列值來進行驗證,若過兩者相同便表示密碼正確。我們首先看看摘要驗證如何運作:
客戶端請求的 URL:
GET / HTTP/1.1
伺服器要求身份驗證:
HTTP/1.1 401 Unauthorized WWW−Authenticate: Digest realm="The batcave", qop="auth", nonce="4993927ba6279", opaque="d8ea7aa61a1693024c4cc3a516f49b3c" HTTP/1.1 401 Unauthorized WWW−Authenticate: Digest realm="The batcave", qop="auth", nonce="4993927ba6279", opaque="d8ea7aa61a1693024c4cc3a516f49b3c"
客戶端驗證:
GET / HTTP/1.1 Authorization: Digest username="admin", realm="The batcave", nonce=49938e61ccaa4, uri="/", response="98ccab4542f284c00a79b5957baaff23", opaque="d8ea7aa61a1693024c4cc3a516f49b3c", qop=auth, nc=00000001, cnonce="8d1b34edb475994b"
來自伺服器的訊息:
| realm | 一個將會用於使用者介面和散列函式的字串。 |
| qop | 它的全寫是 quality of protection,可以是 auth 或 auth-int,它決定散列函式的計算方法,這裡我們使用 auth。 |
| nonce | 一個獨特的代碼,這將會用於散列函式,用戶端需要發回這個代碼。 |
| opaque | 這個可以視為會話 id,改變它的值會使瀏覽器註銷現有用戶的驗證。 |
來自客戶端的訊息:
| username | 提供的用戶名稱。 |
| realm | 與伺服器提供的 realm 相同。 |
| nonce | 與伺服器提供的 nonce 相同。 |
| uri | 用戶請求的 uri。 |
| response | 驗證散列值。 |
| opaque | 與伺服器提供的 opaque 相同。 |
| qop | 與伺服器提供的 qop 相同。 |
| nc | 一個十六進位的計數,每次客戶端送來一個請求這個數值便會遞增。 |
| cnonce | 一個由客戶端產生的 id。 |
我們如何檢查密碼是否正確呢?以下的算法可以用來進行驗證:
4fb7b9011939a2_
用 PHP 來寫便是這樣:
| PHP | | copy code | | ? |
| 1 | A1 = md5(username:realm:password) |
| 2 | A2 = md5(request-method:uri) // request method = GET, POST, etc. |
| 3 | Hash = md5(A1:nonce:nc:cnonce:qop:A2) |
| 4 | if (Hash == response) |
| 5 | //success! |
| 6 | else |
| 7 | //failure! |
正如你看到,我們需要明碼版本的用戶密碼來計算散列值,但是從系統安全的角度,把用戶的密碼儲存在伺服器並非好主意,所以強烈推薦儲存 $A1 的值。
安全改進
- 每次客戶端送來請求的時候,最好順便檢查 opaque、nonce 和 realm 的值,若果伺服器儲存了這些資料,為甚麼不檢查?
- nc 的數值應該越來越大,建議在伺服器儲存這個數值,並確保它沒有任何突然大的跳躍,它未必會嚴格的順序遞增,由於網絡的性質,你收到的數值偶然會缺少一個,或者沒有按順序。
- qop 是一個質量參數,如果 qop 設定為 auth,只有請求的 uri 會被散列函式使用,如果 qop 是 auth-int,請求的內容亦會被散列函式使用。(A2 = md5(request-method:uri:md5(request-body))
註銷驗證
伺服器只要再次送出 HTTP 401,瀏覽器便會註銷先前的驗證資料,從新要求用戶驗證。
相關文章
- 網站開發人員必備的 20 張速記片 (cheat sheet)
- PHP 開發人員比較喜歡以 Windows 為開發平台
- Google 教你優化 PHP,PHP 開發團隊指內容不確
- 認識 PHP 的 Hashing Functions
