章 34. 用 PHP 進行 HTTP 認證

PHP 的 HTTP 認證機制僅在 PHP 以 Apache 模組模式運行時才有效,因此該功能不適用於 CGI 版本。在 Apache 模組的 PHP 腳本中,可以用 header() 函數來向用戶端瀏覽器傳送「Authentication Required」訊息,使其跳出一個會員名/密碼輸入視窗。當會員輸入會員名和密碼後,包括有 URL 的 PHP 腳本將會加上預定義變量 PHP_AUTH_USERPHP_AUTH_PWAUTH_TYPE 被再次呼叫,這三個變量分別被設定為會員名,密碼和認證類型。預定義變量儲存在 $_SERVER 或是 $HTTP_SERVER_VARS 陣列中。支援「Basic」和「Digest」(自 PHP 5.1.0 起)認證方法。請參閱 header() 函數以取得更多訊息。

PHP 版本問題: Autoglobals 全局變量,內含 $_SERVER等,自 PHP 4.1.0 起有效,$HTTP_SERVER_VARS 從 PHP 3 開始有效。

以下是在頁面上強迫用戶端認證的腳本範例:

例子 34-1. Basic HTTP 認證範例

<?php
  
if (!isset($_SERVER['PHP_AUTH_USER'])) {
    
header('WWW-Authenticate: Basic realm="My Realm"');
    
header('HTTP/1.0 401 Unauthorized');
    echo 
'Text to send if user hits Cancel button';
    exit;
  } else {
    echo 
"<p>Hello {$_SERVER['PHP_AUTH_USER']}.</p>";
    echo 
"<p>You entered {$_SERVER['PHP_AUTH_PW']} as your password.</p>";
  }
?>

例子 34-2. Digest HTTP 認證範例

本例演示怎樣實現一個簡單的 Digest HTTP 認證腳本。更多訊息請參考 RFC 2617

<?php
$realm 
'Restricted area';

//user => password
$users = array('admin' => 'mypass''guest' => 'guest');


if (empty(
$_SERVER['PHP_AUTH_DIGEST'])) {
    
header('HTTP/1.1 401 Unauthorized');
    
header('WWW-Authenticate: Digest realm="'.$realm.
           
'" qop="auth" nonce="'.uniqid().'" opaque="'.md5($realm).'"');

    die(
'Text to send if user hits Cancel button');
}


// analyze the PHP_AUTH_DIGEST variable
if (!($data http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) ||
    !isset(
$users[$data['username']]))
    die(
'Wrong Credentials!');


// generate the valid response
$A1 md5($data['username'] . ':' $realm ':' $users[$data['username']]);
$A2 md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
$valid_response md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);

if (
$data['response'] != $valid_response)
    die(
'Wrong Credentials!');

// ok, valid username & password
echo 'Your are logged in as: ' $data['username'];


// function to parse the http auth header
function http_digest_parse($txt)
{
    
// 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+)=([\'"]?)([a-zA-Z0-9=./\_-]+)\2@'$txt$matchesPREG_SET_ORDER);

    foreach (
$matches as $m) {
        
$data[$m[1]] = $m[3];
        unset(
$needed_parts[$m[1]]);
    }

    return 
$needed_parts false $data;
}
?>
   </programlisting>
  </example>
 </para>

 <note>
  <title>相容性問題</title>
  <para>
   在編寫 HTTP
   標頭代碼時請格外小心。為了對所有的用戶端保證相容性,關鍵字「Basic」的第一個字母必須大寫為「B」,分界字串必須用雙引號(不是單引號)引用;並且在標頭行
   <emphasis>HTTP/1.0 401</emphasis> 中,在 <emphasis>401</emphasis> 前必須有且僅有一個空格。
  </para>
 </note>

 <para>
  在以上例子中,僅僅只列印出了 <varname>PHP_AUTH_USER</varname> 和
  <varname>PHP_AUTH_PW</varname>
  的值,但在實際運用中,可能需要對會員名和密碼的合法性進行檢查。或許進行資料庫的查詢,或許從 dbm 檔案中檢索。
 </para>

 <para>
  注意有些 Internet Explorer
  瀏覽器本身有問題。它對標頭的順序顯得似乎有點吹毛求疵。目前看來在傳送
  <literal>HTTP/1.0 401</literal> 之前先傳送
  <emphasis>WWW-Authenticate</emphasis> 標頭似乎可以解決此問題。
 </para>

 <simpara>
  自 PHP 4.3.0
  起,為了防止有人通過編寫腳本來從用傳統外部機制認證的頁面上取得密碼,當外部認證對特定頁面有效,並且&safemode;被開啟時,PHP_AUTH
  變量將不會被設定。但無論如何,<varname>REMOTE_USER</varname>
  可以被用來辨認外部認證的會員,因此可以用
  <varname>$_SERVER['REMOTE_USER']</varname> 變量。
 </simpara>

 <note>
  <title>配置說明</title>
  <para>
   PHP 用是否有 <literal>AuthType</literal> 指令來判斷外部認證機制是否有效。
  </para>
 </note>

 <simpara>
  注意,這仍然不能防止有人通過未認證的 URL 來從同一伺服器上認證的 URL 上偷取密碼。
 </simpara>
 <simpara>
  Netscape Navigator 和 Internet Explorer 瀏覽器都會在收到 401
  的服務端返回訊息時清理所有的本地瀏覽器整個功能變數的 Windows
  認證暫存。這能夠有效的登出一個會員,並迫使他們重新輸入他們的會員名和密碼。有些人用這種方法來使登入狀態「過期」,或是作為「登出」按鈕的響應行為。
 </simpara>
 <para>
  <example>
    <title>強迫重新輸入會員名和密碼的 HTTP 認證的範例</title>
    <programlisting role="php">
<![CDATA[
<?php
  
function authenticate() {
    
header('WWW-Authenticate: Basic realm="Test Authentication System"');
    
header('HTTP/1.0 401 Unauthorized');
    echo 
"You must enter a valid login ID and password to access this resource\n";
    exit;
  }

  if (!isset(
$_SERVER['PHP_AUTH_USER']) ||
      (
$_POST['SeenBefore'] == && $_POST['OldAuth'] == $_SERVER['PHP_AUTH_USER'])) {
   
authenticate();
  }
  else {
   echo 
"<p>Welcome: {$_SERVER['PHP_AUTH_USER']}<br />";
   echo 
"Old: {$_REQUEST['OldAuth']}";
   echo 
"<form action='{$_SERVER['PHP_SELF']}' METHOD='post'>\n";
   echo 
"<input type='hidden' name='SeenBefore' value='1' />\n";
   echo 
"<input type='hidden' name='OldAuth' value='{$_SERVER['PHP_AUTH_USER']}' />\n";
   echo 
"<input type='submit' value='Re Authenticate' />\n";
   echo 
"</form></p>\n";
  }

該行為對於 HTTP 的 Basic 認證標準來說並不是必須的,因此不能依靠這種方法。對 Lynx 瀏覽器的測試顯示 Lynx 在收到 401 的服務端返回訊息時不會清理認證檔案,因此只要對認證檔案的檢查要求沒有變化,只要會員點閱「後退」按鈕,再點閱「前進」按鈕,其原有資源仍然能夠被訪問。不過,會員可以通過按「_」鍵來清理他們的認證訊息。

同時請注意,在 PHP 4.3.3 之前,由於微軟 IIS 的限制,HTTP 認證無法工作在 IIS 伺服器的 CGI 模式下。為了能夠使其在 PHP 4.3.3 以上版本能夠工作,需要編輯 IIS 的設定「目錄安全」。點閱「編輯」並且只選取「匿名訪問」,其它所有的復選框都應該留空。

另一個限制是在 IIS 的 ISAPI 模式下使用 PHP 4 的時候,無法使用 PHP_AUTH_* 變量,而只能使用 HTTP_AUTHORIZATION。例如,考慮如下代碼:list($user, $pw) = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));

IIS 注意事項: 要 HTTP 認證能夠在 IIS 下工作,PHP 配置選項 cgi.rfc2616_headers 必須設定成 0(預設值)。

注: 若果安全模式被啟動,腳本的 UID 會被加到 WWW-Authenticate 標頭的 realm 部分。