2008年11月11日星期二

PHP的HTTP认证机制

PHP 的 HTTP 认证机制仅在 PHP 以 Apache 模块方式运行时才有效,因此该功能不适用于 CGI 版本. 在 Apache 模块的 PHP 脚本中,可以用 header() 函数来向客户端浏览器发送 "Authentication Required" 信息,使其弹出一个用户名 / 密码输入窗口. 当用户输入用户名和密码后,包含有 URL 的 PHP 脚本将会加上 预定义变量 PHP_AUTH_USER , PHP_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 开始有效.

以下是在页面上强迫客户端认证的脚本范例:
[php title="Basic HTTP 认证范例"]
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>";
}
[/php]


本例演示怎样实现一个简单的 Digest HTTP 认证脚本. 更多信息请参考 RFC 2617 .
[php title="Digest HTTP 认证范例"]
$realm = 'Restricted area';

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

if (!isset($_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');
}

// analize the PHP_AUTH_DIGEST variable
preg_match('/username="(?P<username>.*)",
\s*realm="(?P<realm>.*)", \s*nonce="(?P<nonce>.*)",
\s*uri="(?P<uri>.*)", \s*response="(?P<response>.*)",
\s*opaque="(?P<opaque>.*)", \s*qop=(?P<qop>.*),
\s*nc=(?P<nc>.*), \s*cnonce="(?P<cnonce>.*)"/' ,
$_SERVER ['PHP_AUTH_DIGEST'], $digest
);

if (!isset($users[$digest['username']]))
die('Username not valid!' );


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

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

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

兼容性问题: 在编写 HTTP 标头代码时请格外小心. 为了对所有的客户端保证兼容性,关键字 "Basic" 的第一个字母必须大写为 "B" ,分界字符串必须用双引号(不是单引号)引用;并且在标头行 HTTP/1.0 401 中,在 401 前必须有且仅有一个空格.

在以上例子中,仅仅只打印出了 PHP_AUTH_USERPHP_AUTH_PW 的值,但在实际运用中,可能需要对用户名和密码的合法性进行检查. 或许进行数据库的查询,或许从 dbm 文件中检索.

注意有些 Internet Explorer 浏览器本身有问题. 它对标头的顺序显得似乎有点吹毛求疵. 目前看来在发送 HTTP/1.0 401 之前先发送 WWW-Authenticate 标头似乎可以解决此问题.

自 PHP 4.3.0 起,为了防止有人通过编写脚本来从用传统外部机制认证的页面上获取密码,当外部认证对特定页面有效,并且安全模式 被开启时,PHP_AUTH 变量将不会被设置. 但无论如何, REMOTE_USER 可以被用来辨认外部认证的用户,因此可以用 $_SERVER['REMOTE_USER'] 变量.

配置说明: PHP 用是否有 AuthType 指令来判断外部认证机制是否有效.

注意,这仍然不能防止有人通过未认证的 URL 来从同一服务器上认证的 URL 上偷取密码.

Netscape Navigator 和 Internet Explorer 浏览器都会在收到 401 的服务端返回信息时清空所有的本地浏览器整个域的 Windows 认证缓存. 这能够有效的注销一个用户,并迫使他们重新输入他们的用户名和密码. 有些人用这种方法来使登录状态 "过期" ,或者作为 "注销" 按钮的响应行为.

[php title="强迫重新输入用户名和密码的 HTTP 认证的范例"]
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'] == 1 && $_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" ;
}
[/php]

该行为对于 HTTP 的 Basic 认证标准来说并不是必须的,因此不能依靠这种方法. 对 linux 浏览器的测试表明 linux 在收到 401 的服务端返回信息时不会清空认证文件,因此只要对认证文件的检查要求没有变化,只要用户点击 "后退" 按钮,再点击 "前进" 按钮,其原有资源仍然能够被访问. 不过,用户可以通过按 "_" 键来清空他们的认证信息.

同时请注意,在 PHP 4.3.3 之前,由于微软 IIS 的限制,HTTP 认证无法工作在 IIS 服务器的 CGI 模式下. 为了能够使其在 PHP 4.3.3 以上版本能够工作,需要编辑 IIS 的设置 "目录安全" . 点击 "编辑" 并且只选择 "匿名访问" ,其它所有的复选框都应该留空.

另一个限制是在 IIS 的 ISAPI 模式下使用 PHP 4 的时候,无法使用 PHP_AUTH_* 变量,而只能使用 HTTP_AUTHORIZATION . 例如,考虑如下代码:
[php]
list($user, $pw) = explode(':',
base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
[/php]

IIS 注意事项: 要 HTTP 认证能够在 IIS 下工作,PHP 配置选项 cgi.rfc2616_headers 必须设置成 0(默认值).

注: 如果安全模式被激活,脚本的 UID 会被加到 WWW-Authenticate 标头的 realm 部分.

1 条评论: