Skip to main content

用 PHP 實現 HTTP 身份驗證

阿恆's picture
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']!=$validResponserequireLogin($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$matchesPREG_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

  • Lines and paragraphs break automatically.
  • Images can be added to this post.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.