Submitted by 阿恆 on Wed, 2009-02-18 11:38
Posted in
HTTP 身份驗證 (HTTP authentication) 是一種十分常用而容易實作的驗證方法,它倚賴網頁伺服器的內置功能,大量縮短所需編寫的程式碼,對於用戶驗證的要求不高的系統,是一個很實用的驗證方法。Evert Pot 在他的網誌上討論了如何用 PHP 實作這種用戶驗證。
基本驗證 (Basic Auth)
HTTP 身份驗證有兩個主要的驗證方案:「基本驗證 (basic authentication)」和「摘要驗證 (digest authentication)」,其中基本驗證比較容易實作,所以也比較常見,以下是一個以 PHP 實作的基本驗證:
<?php
$username = null;
$password = null;
// 若果有 mod_php
if (isset($_SERVER['PHP_AUTH_USER'])) {
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
// 其他伺服器
} elseif (isset($_SERVER['HTTP_AUTHENTICATION'])) {
if (strpos(strtolower($_SERVER['HTTP_AUTHENTICATION']),'basic')===0)
list($username,$password) = explode(':',base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
}
if (is_null($username)) {
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo 'Text to send if user hits Cancel button';
die();
} else {
echo "<p>Hello {$username}.</p>";
echo "<p>You entered {$password} as your password.</p>";
}
?>這不算很複雜,是嗎?請注意用戶名稱和密碼都是用 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"客戶端驗證:
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。 |
我們如何檢查密碼是否正確呢?以下的算法可以用來進行驗證:A1 = md5(username:realm:password)
A1 = md5(username:realm:password)
A2 = md5(request-method:uri) // request method = GET, POST, etc.
Hash = md5(A1:nonce:nc:cnonce:qop:A2)
if (Hash == response)
//success!
else
//failure!
用 PHP 來寫便是這樣:
<?php
$realm = 'The batcave';
// Just a random id
$nonce = uniqid();
// Get the digest from the http header
$digest = getDigest();
// If there was no digest, show login
if (is_null($digest)) requireLogin($realm,$nonce);
$digestParts = digestParse($digest);
$validUser = 'admin';
$validPass = '1234';
// Based on all the info we gathered we can figure out what the response should be
$A1 = md5("{$digestParts['username']}:{$realm}:{$validPass}");
$A2 = md5("{$_SERVER['REQUEST_METHOD']}:{$digestParts['uri']}");
$validResponse = md5("{$A1}:{$digestParts['nonce']}:{$digestParts['nc']}:{$digestParts['cnonce']}:{$digestParts['qop']}:{$A2}");
if ($digestParts['response']!=$validResponse) requireLogin($realm,$nonce);
// We're in!
echo 'Well done sir, you made it all the way through the login!';
// This function returns the digest string
function getDigest() {
// mod_php
if (isset($_SERVER['PHP_AUTH_DIGEST'])) {
$digest = $_SERVER['PHP_AUTH_DIGEST'];
// most other servers
} elseif (isset($_SERVER['HTTP_AUTHENTICATION'])) {
if (strpos(strtolower($_SERVER['HTTP_AUTHENTICATION']),'digest')===0)
$digest = substr($_SERVER['HTTP_AUTHORIZATION'], 7);
}
return $digest;
}
// This function forces a login prompt
function requireLogin($realm,$nonce) {
header('WWW-Authenticate: Digest realm="' . $realm . '",qop="auth",nonce="' . $nonce . '",opaque="' . md5($realm) . '"');
header('HTTP/1.0 401 Unauthorized');
echo 'Text to send if user hits Cancel button';
die();
}
// This function extracts the separate values from the digest string
function digestParse($digest) {
// protect against missing data
$needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
$data = array();
preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
$data[$m[1]] = $m[2] ? $m[2] : $m[3];
unset($needed_parts[$m[1]]);
}
return $needed_parts ? false : $data;
}
?>
正如你看到,我們需要明碼版本的用戶密碼來計算散列值,但是從系統安全的角度,把用戶的密碼儲存在伺服器並非好主意,所以強烈推薦儲存 $A1 的值。
安全改進
-
每次客戶端送來請求的時候,最好順便檢查 opaque、nonce 和 realm 的值,若果伺服器儲存了這些資料,為甚麼不檢查?
- nc 的數值應該越來越大,建議在伺服器儲存這個數值,並確保它沒有任何突然大的跳躍,它未必會嚴格的順序遞增,由於網絡的性質,你收到的數值偶然會缺少一個,或者沒有按順序。
- qop 是一個質量參數,如果 qop 設定為 auth,只有請求的 uri 會被散列函式使用,如果 qop 是 auth-int,請求的內容亦會被散列函式使用。(A2 = md5(request-method:uri:md5(request-body))
註銷驗證
伺服器只要再次送出 HTTP 401,瀏覽器便會註銷先前的驗證資料,從新要求用戶驗證。


Post new comment