解决CGI/FastCGI Apache下缺少Authorization标头的问题
SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
现在,如果客户端发送Authorization标头,PHP应该自动声明$_SERVER[PHP_AUTH_*]变量。
可以使用 header() 函数向客户端浏览器发送 "Authentication Required"
消息,从而弹出用户名/密码输入窗口。用户填写用户名和密码后,包含PHP脚本的URL将再次被调用,并设置 预定义变量 PHP_AUTH_USER、PHP_AUTH_PW 和 AUTH_TYPE 分别为用户名、密码和身份验证类型。这些预定义变量位于 $_SERVER 数组中。仅支持“Basic”身份验证方法。有关更多信息,请参见 header() 函数。
一个强制客户端在页面上进行身份验证的示例脚本片段如下所示
示例 #1 基本HTTP身份验证示例
<?php
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo '如果用户点击取消按钮,则发送的文本';
exit;
} else {
echo "<p>您好 {$_SERVER['PHP_AUTH_USER']}.</p>";
echo "<p>您输入 {$_SERVER['PHP_AUTH_PW']} 作为您的密码.</p>";
}
?>
注意:兼容性说明
编写HTTP标头行时请谨慎。为了确保与所有客户端的最大兼容性,“Basic”关键字应使用大写“B”,区域字符串必须用双引号(而不是单引号)括起来,并且在HTTP/1.0 401标头行中的401代码前应精确地有一个空格。身份验证参数必须用逗号分隔。
您可以检查用户名和密码的有效性,而不是像上例那样简单地打印出PHP_AUTH_USER和PHP_AUTH_PW。也许可以通过向数据库发送查询,或者在dbm文件中查找用户。
注意那些有问题的Internet Explorer浏览器。它们对标头的顺序似乎非常挑剔。目前,在HTTP/1.0 401
标头之前发送WWW-Authenticate标头似乎可以解决问题。
注意:配置说明
PHP使用
AuthType
指令的存在来确定是否启用了外部身份验证。
但是,请注意,以上方法并不能阻止控制非认证URL的人员从同一服务器上的认证URL窃取密码。
Netscape Navigator和Internet Explorer都会在收到401服务器响应时清除本地浏览器窗口的区域身份验证缓存。这可以有效地“注销”用户,迫使他们重新输入用户名和密码。有些人用它来“超时”登录,或提供“注销”按钮。
示例 #2 强制使用新的用户名/密码的HTTP身份验证示例
<?php
function authenticate() {
header('WWW-Authenticate: Basic realm="Test Authentication System"');
header('HTTP/1.0 401 Unauthorized');
echo "您必须输入有效的登录ID和密码才能访问此资源\n";
exit;
}
if (!isset($_SERVER['PHP_AUTH_USER']) ||
($_POST['SeenBefore'] == 1 && $_POST['OldAuth'] == $_SERVER['PHP_AUTH_USER'])) {
authenticate();
} else {
echo "<p>欢迎: " . htmlspecialchars($_SERVER['PHP_AUTH_USER']) . "<br />";
echo "旧: " . htmlspecialchars($_REQUEST['OldAuth']);
echo "<form action='' method='post'>\n";
echo "<input type='hidden' name='SeenBefore' value='1' />\n";
echo "<input type='hidden' name='OldAuth' value=\"" . htmlspecialchars($_SERVER['PHP_AUTH_USER']) . "\" />\n";
echo "<input type='submit' value='重新验证' />\n";
echo "</form></p>\n";
}
?>
此行为不是HTTP Basic
身份验证标准所要求的,因此您永远不应该依赖此行为。Lynx
测试表明,Lynx
不会使用401服务器响应清除身份验证凭据,因此只要凭据要求没有更改,按后退然后再次向前将打开资源。但是,用户可以按'_'
键清除其身份验证信息。
为了使HTTP身份验证能够与使用CGI版PHP的IIS服务器一起工作,您必须编辑IIS配置“目录安全
”。单击“编辑
”,仅选中“匿名访问
”,其他所有字段应保持未选中状态。
注意:IIS说明:
要使HTTP身份验证与IIS一起工作,PHP指令 cgi.rfc2616_headers必须设置为0
(默认值)。
解决CGI/FastCGI Apache下缺少Authorization标头的问题
SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
现在,如果客户端发送Authorization标头,PHP应该自动声明$_SERVER[PHP_AUTH_*]变量。
这是我找到的最简单的带有重试的基本授权形式。
<?php
$valid_passwords = array ("mario" => "carbonell");
$valid_users = array_keys($valid_passwords);
$user = $_SERVER['PHP_AUTH_USER'];
$pass = $_SERVER['PHP_AUTH_PW'];
$validated = (in_array($user, $valid_users)) && ($pass == $valid_passwords[$user]);
if (!$validated) {
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
die ("未授权");
}
// 如果到达此处,则表示用户有效。
echo "<p>欢迎 $user.</p>";
echo "<p>恭喜您,您已进入系统。</p>";
?>
对于使用CGI的PHP,请确保将上述重写规则放在任何其他重写规则之前。
在我的例子中,我把它放在.htaccess文件的顶部(在RewriteEngine On之后)。
RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization}]
我的问题是REMOTE_USER(或者我的情况下的REDIRECT_REMOTE_USER)根本没有设置。
原因:我有一些其他的RewriteRule生效了,并且被设置为LAST规则。
希望这有帮助。
对于CGI/FastCGI,您将无法访问PHP_AUTH*信息,因为CGI协议没有声明此类变量(这就是它们的名字以PHP开头的原因),并且服务器不会将它们传递给解释器。在CGI中,服务器应该自己验证用户,并在之后将REMOTE_USER传递给CGI脚本。
因此,您需要以某种方式“获取”请求头并将其传递给您的脚本。
在Apache中,如果安装了mod_env,您可以通过环境变量来实现。
在.htaccess中,以下结构将请求头“Authorization”复制到env变量PHP_AUTH_DIGEST_RAW
SetEnvIfNoCase ^Authorization$ "(.+)" PHP_AUTH_DIGEST_RAW=$1
您现在可以通过$_ENV访问它。
不要忘记从您的环境变量中去除身份验证类型(在我的例子中是“Digest”),因为PHP_AUTH_DIGEST没有它。
如果未安装mod_env,您可能安装了mod_rewrite(每个人都安装了它,因为“人类可读的URL”)。
您可以获取标头并使用重写规则将其作为GET参数传递
RewriteRule ^.*$ site.php?PHP_AUTH_DIGEST_RAW=%{HTTP:Authorization} [NC,L]
在这里,HTTP请求头Authorization可以通过$_GET作为PHP_AUTH_DIGEST_RAW访问。
---
如果您使用ZF,您可能使用Zend_Auth_Adapter_Http来验证用户。
它使用“Zend_Controller_Request::getHeader”获取Authorization信息。
此方法使用apache_request_header,在旧的CGI/FastCGI安装中可能无法访问,或者使用_$_SERVER['HTTP_<HeaderName>'],因此您需要将通过_GET或ENV获得的身份验证数据放入
_$_SERVER['HTTP_AUTHORIZATION']。
这将使ZF与您的解决方案透明地工作,我相信任何其他框架也应该工作。
回到CGI模式下的授权。这是一个完整的可工作示例
#创建一个包含以下内容的.htaccess文件
#您也可以使用条件(在此页面上搜索)
RewriteEngine on
RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization},L]
#在开始检查授权的脚本中,放置以下代码
$userpass = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"],6)) ;
$userpass = explode(":", $userpass);
if ( count($userpass) == 2 ){
#这部分不适用于所有情况。
#print_r($userpass);die; #<- 这可以帮助找出正确的用户名和密码
list($name, $password) = explode(':', $userpass);
$_SERVER['PHP_AUTH_USER'] = $name;
$_SERVER['PHP_AUTH_PW'] = $password;
}
示例#2中的http_digest_parse中的正则表达式对我不起作用(PHP 5.2.6),因为字符类中不允许反向引用。这个对我有用
<?php
// 解析http auth header的函数
function http_digest_parse($txt)
{
// 防止数据丢失
$needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
$data = array();
preg_match_all('@(\w+)=(?:(?:\'([^\']+)\'|"([^"]+)")|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
$data[$m[1]] = $m[2] ? $m[2] : ($m[3] ? $m[3] : $m[4]);
unset($needed_parts[$m[1]]);
}
return $needed_parts ? false : $data;
}
?>
你不应该在RewriteRule中使用“last”(“L”)指令!这将阻止在给出Basic或Digest Auth时跳过所有后续的重写规则,这几乎肯定不是你想要的。
因此,以下几行对于.htaccess(或httpd.conf)文件就足够了
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
这是我尝试创建一个摘要身份验证类,它可以在不使用cookie、会话、数据库或文件的情况下登录和注销用户。其核心是这段简单的代码,用于将摘要字符串解析为变量,适用于多个浏览器。
<?php
// Tony Wyatt于2007年6月21日编写,支持多种浏览器的摘要解析
public function explodethedigest($instring) {
$quote = '"';
$equal = '=';
$comma = ',';
$space = ' ';
$a = explode( $comma, $instring);
$ax = explode($space, $a[0]);
$b = explode( $equal, $ax[1], 2);
$c = explode( $equal, $a[1], 2);
$d = explode( $equal, $a[2], 2);
$e = explode( $equal, $a[3], 2);
$f = explode( $equal, $a[4], 2);
$g = explode( $equal, $a[5], 2);
$h = explode( $equal, $a[6], 2);
$i = explode( $equal, $a[7], 2);
$j = explode( $equal, $a[8], 2);
$k = explode( $equal, $a[9], 2);
$l = explode( $equal, $a[10], 2);
$parts = array(trim($b[0])=>trim($b[1], '"'), trim($c[0])=>trim($c[1], '"'), trim($d[0])=>trim($d[1], '"'), trim($e[0])=>trim($e[1], '"'), trim($f[0])=>trim($f[1], '"'), trim($g[0])=>trim($g[1], '"'), trim($h[0])=>trim($h[1], '"'), trim($i[0])=>trim($i[1], '"'), trim($j[0])=>trim($j[1], '"'), trim($k[0])=>trim($k[1], '"'), trim($l[0])=>trim($l[1], '"'));
return $parts;
}
?>
请访问 http://www.creativetheory.ca/ /tests/ta1.php 进行尝试。使用用户名test和密码pass或用户名guest和密码guest登录。访问第二页查看代码链接。欢迎提出评论、想法、建议或批评。
我想出了另一种方法来解决浏览器缓存WWW身份验证凭据并导致注销问题的问题。虽然大多数浏览器都有一些清除此信息的方法,但我更希望我的网站能够处理此任务,而不是依赖于用户的理智。
即使使用Lalit创建随机领域名称的方法,仍然可以使用Firefox的后退按钮返回受保护区域,因此该方法无效。这是我的解决方案。
由于浏览器将凭据附加到特定URL,因此请使用虚拟路径,其中路径的一个组件实际上是PHP脚本,其后的一切都是URI的一部分,例如
http://velocitypress.ca/some_dir/login.php/auth/8f631b92/
通过为URL的最后一个组件选择不同的数字,可以欺骗浏览器使其认为它们正在处理完全不同的网站,从而再次提示用户输入凭据。
请注意,使用随机的、不受限制的数字仍然允许用户点击后退按钮返回页面。您应该在服务器端文件或数据库中跟踪此数字,并在每次成功登录后重新生成它,以便最后一个数字无效。使用无效的数字可能会导致403响应,或者,取决于您当天的感觉,导致302重定向到一个讨厌的网站。
在这种情况下,从生成的页面链接时应小心,因为相对链接将相对于虚拟的、不存在的目录,而不是相对于真实的脚本目录。
关于……的帖子的更简单方法
bernard dot paques at bigfoot dot com
2004年9月24日 01:42
这是对将PHP作为CGI运行时PHP_AUTH_USER和PHP_AUTH_PW服务器变量问题的另一个“补丁”。
首先,不要忘记在您的.htaccess文件中添加这段代码(这是使其与mod_rewrite一起工作的唯一需要的东西)
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization},L]
</IfModule>
然后是login.php
<?php
$a = base64_decode( substr($_SERVER["REMOTE_USER"],6)) ;
if ( (strlen($a) == 0) || ( strcasecmp($a, ":" ) == 0 ))
{
header( 'WWW-Authenticate: Basic realm="Private"' );
header( 'HTTP/1.0 401 Unauthorized' );
}
else
{
list($name, $password) = explode(':', $a);
$_SERVER['PHP_AUTH_USER'] = $name;
$_SERVER['PHP_AUTH_PW'] = $password;
}
echo 'PHP_AUTH_USER =' . $_SERVER['PHP_AUTH_USER'] . '<br>';
echo 'PHP_AUTH_PW =' . $_SERVER['PHP_AUTH_PW'] . '<br>';
echo 'REMOTE_USER =' . $_SERVER['REMOTE_USER'] . '<br>';
?>
首先,我们解码Base64编码的字符串,丢弃“Basic ”开头的6个字符,然后进行常规验证。
脚本结束时,我们打印变量以验证其是否正常工作。在生产版本中应省略此步骤。
这是Bernard Paques脚本的一个变体。
感谢他提供这段代码。
我无法使用任何示例使身份验证正常工作。最后,我从Zend的教程示例开始:
http://www.zend.com/zend/tut/authentication.php?article=authentication(使用.htpasswd验证)并尝试处理其他情况。我的总体结论是,更改域是唯一可靠的方式来使浏览器再次询问,并且我要感谢将该示例放入手册中的人,因为它让我走上了正确的道路。无论如何,浏览器都拒绝丢弃它已经记住的值。当然,更改域的问题是,您不希望在一个给定的会话中执行此操作,否则它会导致新的密码请求。所以,就这样,希望复制粘贴不会太乱。
我花了大部分时间让它正常工作。我很难理解浏览器遇到身份验证请求时会做什么:在我看来,它尝试获取密码,然后重新加载页面……因此HTML不会运行。至少,IE是这样,我没有用其他东西测试过。
<?php
session_start() ;
if (!isset($_SESSION['realm'])) {
$_SESSION['realm'] = mt_rand( 1, 1000000000 ).
" SECOND level: Enter your !!!COMPANY!!! password.";
header( "WWW-Authenticate: Basic realm=".$_SESSION['realm'] );
// 下面的代码只有在没有 $_SESSION 时才以 HTML 方式运行,
// 并且浏览器 *无法* 设置 $PHP_AUTH_USER……通常
// 浏览器在获得身份验证信息后,会再次运行页面
// 而不会到这里。
// 我基本上想说的是,到这里的方法是跳过登录屏幕。
// 我最初尝试在这里放置一个 session_destroy(),但是
// 问题是 PHP 无论如何都会运行,所以 REFRESH 看起来
// 是处理它的最佳方法。
echo "<meta http-equiv=\"REFRESH\"
content=\"0;url=index.php\">" ;
exit;
}
if ($_POST['logout'] == "logout") {
session_destroy() ;
header('Location: comeagain.php');
exit ;
}
// 这里的“标准”身份验证代码,来自上面的 Zend 教程。
comeagain.php 如下所示:
<?
session_start();
unset($_SESSION['realm']);
session_destroy();
echo "<html><head><title>Logged Out</title><h1>Logout Page</h1><body>" ;
echo "You have successfully logged out of TOGEN";
echo " at ".date("h:m:s")." on ".date("d F Y") ;
echo "<p><a href=\"index.php\">Login Again</a>" ;
echo "</body></html>" ;
?>
其思想是能够丢弃会话(从而重置域),而不会提示浏览器再次询问……因为它已被重定向到 logout.php。
通过这种组合,我可以使事情正常工作。只需确保不要同时运行 Apache 的 htpasswd 身份验证,否则事情会变得非常奇怪 :-)。
请注意,Microsoft 发布了一个“安全更新”,该更新禁用了在 http url 中使用 username:password@host。
http://support.microsoft.com/default.aspx?scid=kb;en-us;834489
不幸的是,上述依赖于此的方法将不再适用于 Microsoft 浏览器。
您可以按照以下说明重新启用此功能:
http://weblogs.asp.net/cumpsd/archive/2004/02/07/69366.aspx
但是您的用户可能不愿意这样做。
要使其与 IIS 一起工作,请尝试在设置“$auth = 0”和“if (isset($PHP_AUTH_USER) && isset($PHP_AUTH_PW))”之前使用此代码
<?php
//////////////////////////////////////////
if ($PHP_AUTH_USER == "" && $PHP_AUTH_PW == "" && ereg("^Basic ", $HTTP_AUTHORIZATION))
{
list($PHP_AUTH_USER, $PHP_AUTH_PW) =
explode(":", base64_decode(substr($HTTP_AUTHORIZATION, 6)));
}
//////////////////////////////////////////
?>
它在我的 IIS 5 和 ISAPI 中的 PHP 4 上运行良好。
在 Windows 2003 Server/IIS6 和 php4+ cgi 中,我的 HTTP 身份验证只能与以下代码一起工作:
<?php header("Status: 401 Access Denied"); ?>
而
<?php header('HTTP/1.0 401 Unauthorized'); ?>
不起作用!
我还需要在“自定义错误”中选择“401;1”到“401;5”的范围,然后单击“设置为默认值”按钮。
感谢 rob at theblip dot com
如果您必须在使用 `http_digest_parse` 函数验证响应*之前*使用 `setlocale` 函数,请小心使用 HTTP Digest 身份验证(参见上文,示例 34.2),因为 `preg_match_all` 函数的模式与 \w 存在冲突。
事实上,由于 \w 应该表示任何字母、数字或下划线字符,您必须记住,这可能会因您的区域设置配置而异(例如,它在法语中接受重音字母)……
由于 `preg_match_all` 函数对模式的不同解释,如果您修改了区域设置(我的意思是如果您的区域设置接受一些扩展字符),`http_digest_parse` 函数将始终返回错误的结果,请参阅 http://fr.php.net/manual/en/reference.pcre.pattern.syntax.php 获取更多信息。
我认为,我建议您在身份验证完成之前不要使用 setlocale……
PS:这是一个不兼容的 setlocale 声明……
setlocale ( LC_ALL, 'fr_FR', 'fr', 'FR', 'french', 'fra', 'france', 'French', 'fr_FR.ISO8859-1' ) ;
您好。
对不起我的英语。
此示例显示了“登录”、“注销”和“重新登录”的编程。
此脚本必须用于受保护的页面。
要使此脚本工作,浏览器地址字符串必须如下所示:
"https://127.0.0.1/admin/?login" - 登录
"https://127.0.0.1/admin/?logout" - 注销
"https://127.0.0.1/admin/?logout&login" - 重新登录
<?php
session_start();
$authorized = false;
# 注销
if (isset($_GET['logout']) && !isset($_GET["login"]) && isset($_SESSION['auth']))
{
$_SESSION = array();
unset($_COOKIE[session_name()]);
session_destroy();
echo "正在注销...";
}
# 检查登录名和密码
if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']))
{
$user = 'test';
$pass = 'test';
if (($user == $_SERVER['PHP_AUTH_USER']) && ($pass == ($_SERVER['PHP_AUTH_PW'])) && isset($_SESSION['auth']))
{
$authorized = true;
}
}
# 登录
if (isset($_GET["login"]) && !$authorized ||
# 重新登录
isset($_GET["login"]) && isset($_GET["logout"]) && !isset($_SESSION['reauth']))
{
header('WWW-Authenticate: Basic Realm="请登录"');
header('HTTP/1.0 401 Unauthorized');
$_SESSION['auth'] = true;
$_SESSION['reauth'] = true;
echo "立即登录,否则永远无法点击...";
exit;
}
$_SESSION['reauth'] = null;
?>
<h1>您已<? echo ($authorized) ? (isset($_GET["login"]) && isset($_GET["logout"]) ? '重新' : '') : '未'; ?>登录!</h1>
新的身份验证
<?php
$login = 'test_login';
$pass = 'test_pass';
if(($_SERVER['PHP_AUTH_PW']!= $pass || $_SERVER['PHP_AUTH_USER'] != $login)|| !$_SERVER['PHP_AUTH_USER'])
{
header('WWW-Authenticate: Basic realm="测试身份验证"');
header('HTTP/1.0 401 Unauthorized');
echo '身份验证失败';
exit;
}
?>
Apache HTTP Server 2.4.13及更高版本中,针对缺少授权标头的CGI/FastCGI的更简单的解决方法
CGIPassAuth On
请不要启用带有基本身份验证的授权标头,它非常不安全。
某些服务器不支持HTTP1.0规范,并将返回500错误(例如)。在我的上传身份验证脚本的服务器上就发生了这种情况。
如果发生这种情况,您可以尝试HTTP1.1标头语法
<?php
header("WWW-Authenticate: Basic realm=\"我的领域\"");
header('status: 401 Unauthorized');
?>
在德语示例#2(摘要)中,<?php $realm = "Geschützter Bereich"; ?>。据我测试,umlaut ü是有问题的,会导致密码输入无限循环。在我的情况下,它是用UTF-8编写的。因此,我建议仅对领域使用纯ASCII字符。
我使用了Louis的示例(2006年6月3日),它对我很有效(谢谢)。
但是,我添加了一些行,以确保用户只能几次获得身份验证窗口。
<?php
$realm = mt_rand( 1, 1000000000)."@YourCompany";
$_SESSION['realm'] = $realm;
// 在开始时,当定义领域时:
$_SESSION['CountTrials'] = 1;
?>
然后在检查身份验证时(ZEND教程)
<?php
// 不超过3次尝试
if (!$auth) {
$_SESSION['CountTrials']++;
if ($_SESSION['CountTrials'] == 4) {
session_destroy() ;
header('Location: noentry.php');
exit ;
} else {
header("WWW-Authenticate: Basic realm=".$_SESSION['realm']);
header("HTTP/1.0 401 Unauthorized");
echo '需要授权.';
exit;
}
} else {
echo '<P>您已授权!</P>';
}
?>
noentry.php与comeagain.php略有不同。
链接到 http://logout:logout@<?=$_SERVER['HTTP_HOST'];?>/SECRET/ 如果不存在密码为logout的用户注销,则会强制对/SECRET目录进行重新登录。
[danbrown AT php DOT net注:以下注释由“匿名”于2010年4月1日添加(尽管我们认为它不是愚人节玩笑)。]
这种注销方法不再100%有效,因为M$又出了另一个垃圾
http://support.microsoft.com/kb/834489
一个使用基本身份验证回退的SSL客户端证书身份验证的简单脚本。我在我的网站上使用LDAP服务器来检查用户名/密码和客户端证书到用户的映射。
<?
// 检查我们是如何进行身份验证的
if ($_SERVER['SSL_CLIENT_VERIFY'] != "SUCCESS") { // 没有使用客户端证书
if ((!$_SERVER['PHP_AUTH_USER']) && (!$_SERVER['PHP_AUTH_PW'])) { // 未使用基本身份验证登录
authenticate(); // 发送基本身份验证标头
}
}
if ($_SERVER['SSL_CLIENT_S_DN_CN'] != "chris") { // 检查证书的CN名称
if (!(($_SERVER['PHP_AUTH_USER'] == "test") && ($_SERVER['PHP_AUTH_PW'] == "123"))) { // 检查用户名和密码
authenticate(); // 发送基本身份验证标头,因为用户名和/或密码不匹配
}
}
phpinfo();
// 调用身份验证显示
function authenticate() {
Header("WWW-Authenticate: Basic realm=Website");
Header("HTTP/1.0 401 Unauthorized");
error401();
exit;
}
?>
有关使用Apache和PHP的客户端证书的更多详细信息,请参阅我的网站(http://velocitypress.ca/index.php?page=/manuals/)。
在我的php-cgi配置中——设置RewriteRule后——正确的变量是:$_SERVER['REDIRECT_HTTP_AUTHORIZATION']
所以PhpCGI的解决方法是
在你的.htaccess中设置
RewriteEngine on
RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization},last]
Php解决方法
<?php
// 为 Apache+php-cgi 绕过设置 HTTP 认证头
if (isset($_SERVER['HTTP_AUTHORIZATION']) && preg_match('/Basic\s+(.*)$/i', $_SERVER['HTTP_AUTHORIZATION'], $matches)) {
list($name, $password) = explode(':', base64_decode($matches[1]));
$_SERVER['PHP_AUTH_USER'] = strip_tags($name);
$_SERVER['PHP_AUTH_PW'] = strip_tags($password);
}
// 如果变量被 Apache 重命名,则为 Apache+php-cgi 绕过设置 HTTP 认证头
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && preg_match('/Basic\s+(.*)$/i', $_SERVER['REDIRECT_HTTP_AUTHORIZATION'], $matches)) {
list($name, $password) = explode(':', base64_decode($matches[1]));
$_SERVER['PHP_AUTH_USER'] = strip_tags($name);
$_SERVER['PHP_AUTH_PW'] = strip_tags($password);
}
?>
我们这里有 .htaccess 文件,它对我们(cPanel + phpsuexec)有效,除非其他方法失败。也许它可以帮助其他人。
#.htaccess: 使用 ModRewrite 的 PHP(CGI 模式)HTTP 授权
RewriteEngine on
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]
然后你需要一小段 php 代码来解析这一行,然后一切都会像 mod_php 一样工作
if (isset($_SERVER['HTTP_AUTHORIZATION']))
{
$ha = base64_decode( substr($_SERVER['HTTP_AUTHORIZATION'],6) );
list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = explode(':', $ha);
unset $ha;
}
享受!
我发现唯一有效的清除 PHP_AUTH_DIGEST 或 PHP_AUTH_USER 和 PHP_AUTH_PW 凭据的方法是调用 header HTTP/1.1 401 Unauthorized。
function clear_admin_access(){
header('HTTP/1.1 401 Unauthorized');
die('管理员访问已关闭');
}
#.htaccess: 使用 ModRewrite 的 PHP(CGI 模式)HTTP 授权
# 最正确的示例,带有对非空值的 header 检查
RewriteEngine on
RewriteCond %{HTTP:Authorization} !^$
RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization}, \
E=PHP_AUTH_USER:%{HTTP:Authorization},L]
关于 tigran at freenet dot am 编写的 php+mysql 身份验证代码
存在一些安全漏洞。
首先
$user
和
$pass
都不安全,它们可能使这段代码容易受到 SQL 注入攻击,你应该始终删除两者中的无效字符,或者至少对它们进行编码。
实际上,将密码存储为 MD5 哈希可以减少你的安全工作量。
第二个安全风险
相同的 mysql 用户具有更新和选择权限,甚至可能在你的身份验证数据库中拥有插入权限。
同样,可能会发生 SQL 注入攻击,最终用户随后可以更改用户的用户名、密码或与之相关的任何其他内容。
第三个问题更多的是性能问题:
你真的需要更新数据库吗?更新比选择慢,如果你每次访问页面都执行更新,就会导致一定的性能损失。
一个选项是,如果你想使用 sql(我认为 mysql 有),可以使用内存数据库,并在内存中创建一个表,该表为每个登录的用户存储一个唯一的会话标识符,或者如果它是一个单一前端系统,你可以使用 db 文件。
public function loginUser(){
$username = filter_var($_GET['username'],FILTER_SANITIZE_STRING);
$pw = filter_var($_GET['pw'],FILTER_SANITIZE_STRING);
$row = $this->userModel->selectUser($username);
if (password_verify($pw,$row['pw'])){
$_SESSION["userId"] = $row['id'];
$this->isLogin();
} else{
new Msg(true,"用户名和密码不匹配!");
}
}
一个简短的用户登录示例
要强制使用基本身份验证注销,你可以更改其下的 Realm 为不同的 Realm。
这会强制你的服务器为新的“Realm”提供一组新的凭据。
你只需要使用用户名/密码跟踪 Realm 名称,并在他们登录和注销时将其更改为新的/随机的名称。
我相信这是唯一一种 100% 保证在 HTTP 基本身份验证中注销的方法,如果它是文档的一部分,这里许多糟糕的用户贡献评论都可以删除。
它每次访问页面时都会强制进行身份验证
(也许可以帮助某人)
<?
header("Expires: Sat, 01 Jan 2000 00:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: post-check=0, pre-check=0",false);
header("Pragma: no-cache");
session_cache_limiter("public, no-store");
session_start();
function http_auth()
{
$_SESSION['AUTH'] = 1;
header('HTTP/1.0 401 Unauthorized');
header('WWW-Authenticate: Basic realm="sn4g auth system"');
// 用户点击“取消”时要执行的操作
exit();
}
if( !isset($_SERVER['PHP_AUTH_USER']) or @$_SESSION['AUTH'] != 1 )
{
http_auth();
exit();
}
// 用户登录后要执行的操作
// 其余部分,必须清除会话数组
$_SESSION = array();
session_destroy();
?>
对于任何尝试过上面摘要示例但没有使其工作的人。
对我来说,问题似乎是使用了已弃用的 '\'(反斜杠)而不是 '$'(美元符号)在正则表达式中表示反向引用。此外,结果必须从剩余的双引号和单引号中删除。
这是一个有效的示例
// 解析 http 认证头的函数
function http_digest_parse($txt)
{
// 防止数据丢失
$needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
$data = array();
preg_match_all('@(\w+)=(?:([\'"])([^$2]+)$2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
$data[$m[1]] = $m[3] ? trim($m[3],"\",'") : trim($m[4],"\",'");
unset($needed_parts[$m[1]]);
}
return $needed_parts ? false : $data;
}
可能有一种更复杂的方法可以在正则表达式中修剪引号,但我不想费心 :-)
致敬,Lars
嗯,我认为让身份验证正常工作很容易。我使用一个会话变量来强制用户每次访问登录区域时都进行身份验证。
<?php
if (!isset ($_SESSION['firstauthenticate'])) {
session_start();
}
function authenticate() {
header('WWW-Authenticate: Basic realm="Sistema autentificaci?n UnoAutoSur"');
header('HTTP/1_0 401 Unauthorized');
// header("Status: 401 Access Denied");
echo "Unauthorized\n";
exit;
}
if (!isset($_SERVER['PHP_AUTH_USER']) || strcmp ($_SERVER['PHP_AUTH_USER'],$user)!=0 ||
!isset ($_SERVER['PHP_AUTH_PW']) || strcmp($_SERVER['PHP_AUTH_PW'],$pass)!=0 || !isset ($_SESSION['firstauthenticate']) || !$_SESSION['firstauthenticate']) {
$_SESSION['firstauthenticate']=true;
authenticate();
} else {
// 我现在销毁会话变量
session_unset();
// 你的代码如下
}
?>
我建议向 Web 服务器(通过 .htaccess 等)请求用户的身份验证和管理。
1. 使用 .htaccess 文件配置受限访问的全局 /logon/ 目录
2. 使用 fopen 包装器
$hh = @fopen("http://{$_SERVER['PHP_AUTH_USER']}:{$_SERVER['PHP_AUTH_PW']}".
@{$_SERVER['SERVER_NAME']}/logon/", "r");
if (!$hh) authenticate(); // 通常的 header WWW-Authenticate ...
fclose($hh);
虽然摘要式身份验证仍然远优于基本身份验证,但仍有一些安全问题需要注意。
在这方面,上面给出的摘要示例存在缺陷,因为nonce永不过期或失效。因此,它变成了密码等效物(尽管仅限于该特定URL),窃听者可以随时使用它来获取页面,从而允许攻击者始终访问页面的最新版本,或者(更糟糕的是)重复调用CGI脚本——例如,如果用户请求URL“/filemanager?delete=somefile”,攻击者可以在将来任何时间重复此删除操作,可能是在文件重新创建之后。
虽然可能无法在无需重新验证的情况下更改GET数据,但可以更改Cookie和POST数据。
为了防止第一个问题,可以使nonce包含时间戳,并添加检查以确保超过例如30分钟的nonce会导致新的身份验证请求。
为了解决第二个问题,需要生成一次性nonce——也就是说,必须拒绝使用特定nonce的所有后续请求。
一种方法是:当用户请求“deletefile”等操作时,将随机生成的nonce存储在会话变量中,使用该nonce发出401身份验证质询,然后在接收身份验证时(并清除会话变量)针对存储的值进行检查。
这样,虽然可能的窃听者会收到nonce并因此获得执行操作的能力,但他只能执行一次——而用户本来就要执行该操作。(只有用户或攻击者,而不是两者都能执行该操作,因此它是安全的。)
当然,在某些时候,只有切换到HTTPS/SSL/TLS才能提高安全性(例如,这是防御中间人攻击的唯一方法)。您可以决定安全级别。
不要使用明文Apache身份验证。最好使用自己的脚本生成与密码相关的新的ID。Apache身份验证数据会发送到每个页面,因此可能存在的错误是已知的。
我尝试了示例7,一开始无法使其工作。我花了一段时间才发现,在某处,可能是服务器,一个看似随机的数字被添加到realm中——因此valid_result变量没有使用正确的realm进行计算。
为了解决这个问题或任何类似的问题,请对示例进行以下更改
大约第43行(如果在下一步之后则为44行)
$needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1, 'realm'=>1);
第24行之前
$realm = $data['realm'];
这两个步骤获取用于身份验证请求的真实realm,并将其替换到“valid_response”查询中。
希望这有帮助 :)
首先,对不起我的英语。
另一个带有注销解决方案的授权脚本。
在meint_at_meint_dot_net提供的脚本中 (https://php.net/manual/en/features.http-auth.php#93859) 使用了rewrite_module。如果该模块有任何问题,并且 Web 服务器的管理权限受到限制(包括使用.htaccess的限制),则可以使用此解决方案。
index.php
---------
<?php
$auth_realm = '我的领域';
require_once 'auth.php';
echo "您已登录为 {$_SESSION['username']}<br>";
echo '<p><a href="?action=logOut">注销</a></p>'
?>
auth.php
--------
<?php
$_user_ = 'test';
$_password_ = 'test';
session_start();
$url_action = (empty($_REQUEST['action'])) ? 'logIn' : $_REQUEST['action'];
$auth_realm = (isset($auth_realm)) ? $auth_realm : '';
if (isset($url_action)) {
if (is_callable($url_action)) {
call_user_func($url_action);
} else {
echo '函数不存在,请求终止';
};
};
function logIn() {
global $auth_realm;
if (!isset($_SESSION['username'])) {
if (!isset($_SESSION['login'])) {
$_SESSION['login'] = TRUE;
header('WWW-Authenticate: Basic realm="'.$auth_realm.'"');
header('HTTP/1.0 401 Unauthorized');
echo '您必须输入有效的登录名和密码';
echo '<p><a href="?action=logOut">重试</a></p>';
exit;
} else {
$user = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : '';
$password = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '';
$result = authenticate($user, $password);
if ($result == 0) {
$_SESSION['username'] = $user;
} else {
session_unset($_SESSION['login']);
errMes($result);
echo '<p><a href="">重试</a></p>';
exit;
};
};
};
}
function authenticate($user, $password) {
global $_user_;
global $_password_;
if (($user == $_user_)&&($password == $_password_)) { return 0; }
else { return 1; };
}
function errMes($errno) {
switch ($errno) {
case 0:
break;
case 1:
echo '您输入的用户名或密码不正确';
break;
default:
echo '未知错误';
};
}
function logOut() {
session_destroy();
if (isset($_SESSION['username'])) {
session_unset($_SESSION['username']);
echo "您已成功注销<br>";
echo '<p><a href="?action=logIn">登录</a></p>';
} else {
header("Location: ?action=logIn", TRUE, 301);
};
if (isset($_SESSION['login'])) { session_unset($_SESSION['login']); };
exit;
}
?>
关于在带有php cgi 4.3.4的IIS中进行HTTP身份验证,还有一个步骤。我努力搜索,但没有在其他任何地方找到此信息,所以就这样吧。在使用PHP CGI进行HTTP身份验证时,您需要执行以下操作
1. 在您的php.ini文件中,设置“cgi.rfc2616_headers = 0”
2. 在“网站属性”->“文件/目录安全”->“匿名访问”对话框中,选中“匿名访问”复选框,并取消选中任何其他复选框(即,如果启用了“基本身份验证”、“集成 Windows 身份验证”和“摘要”,则取消选中它们)。单击“确定”。
3. 在“自定义错误”中,选择范围“401;1”到“401;5”,然后单击“设置为默认值”按钮。
这最后一步至关重要,但在任何地方都没有记录。如果不这样做,IIS不会发出请求凭据的标头,而是返回其自身精美但无用的“您未经身份验证”页面。但如果您这样做,则浏览器将正确地请求凭据,并在$_SERVER['PHP_AUTH_*']元素中提供它们。
我想出了另一种方法来解决浏览器缓存WWW身份验证凭据并导致注销问题的问题。虽然大多数浏览器都有一些清除此信息的方法,但我更希望我的网站能够处理此任务,而不是依赖于用户的理智。
即使使用Lalit创建随机领域名称的方法,仍然可以使用Firefox的后退按钮返回受保护区域,因此该方法无效。这是我的解决方案。
由于浏览器将凭据附加到特定URL,因此请使用虚拟路径,其中路径的一个组件实际上是PHP脚本,其后的一切都是URI的一部分,例如
http://www.example.com/some_dir/login.php/auth/8f631b92/
通过为URL的最后一个组件选择不同的数字,可以欺骗浏览器使其认为它们正在处理完全不同的网站,从而再次提示用户输入凭据。
请注意,使用随机的、不受限制的数字仍然允许用户点击后退按钮返回页面。您应该在服务器端文件或数据库中跟踪此数字,并在每次成功登录后重新生成它,以便最后一个数字无效。使用无效的数字可能会导致403响应,或者,取决于您当天的感觉,导致302重定向到一个讨厌的网站。
在这种情况下,从生成的页面链接时应小心,因为相对链接将相对于虚拟的、不存在的目录,而不是相对于真实的脚本目录。
希望这对某些人有所帮助。
<?php
// 尝试模仿cPanel注销样式
// 唯一的区别是重新登录时usr和pwd字段被清除
// 已在ff2和ie8上测试
session_start();
$username = "test";
$password = "test";
if(isset($_GET['logout']))
{
unset($_SESSION["login"]);
echo "您已注销 ... ";
echo "[<a href='" . $_SERVER['PHP_SELF'] . "'>登录</a>]";
exit;
}
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW']) || !isset($_SESSION["login"]))
{
header("WWW-Authenticate: Basic realm=\"Test\"");
header("HTTP/1.0 401 Unauthorized");
$_SESSION["login"] = true;
echo "您未授权 ... ";
echo "[<a href='" . $_SERVER['PHP_SELF'] . "'>登录</a>]";
exit;
}
else
{
if($_SERVER['PHP_AUTH_USER'] == $username && $_SERVER['PHP_AUTH_PW'] == $password)
{
echo "您已登录 ... ";
echo "[<a href='" . $_SERVER['PHP_SELF'] . "?logout'>注销</a>]";
}
else
{
unset($_SESSION["login"]);
header("Location: " . $_SERVER['PHP_SELF']);
}
}
// 内容在此
?>
一个相当不错的注销问题的解决方案
只需告诉浏览器它已成功登录!
工作原理如下
1. 用户点击注销按钮
2. 脚本发送401头部
3. 用户没有输入密码
4. 如果没有输入密码,脚本发送200头部
因此浏览器不会记住任何密码作为有效密码。
示例
<?php
if (
(!isset($_SERVER['PHP_AUTH_USER']))
||(
($_GET["login"]=="login")
&& !(
($_SERVER['PHP_AUTH_USER']=="validuser")
&& ($_SERVER['PHP_AUTH_PW']=="validpass")
)
)
)
||(
($_GET["logout"]=="logout")
&& !($_SERVER['PHP_AUTH_PW']=="")
)
) {
Header("WWW-Authenticate: Basic realm=\"Realm\"");
Header("HTTP/1.0 401 Unauthorized");
echo "未注销...<br>\n";
echo "<a href=\"index.php?login=login\">登录</a>";
exit;
} else if ($_SERVER['PHP_AUTH_PW']=="") {
echo "已注销...<br>\n";
echo "<a href=\"index.php?login=login\">登录</a>";
exit;
}
?>
登录/注销脚本示例
login.php
<?php
// 初始化
if ($_COOKIE["SESSION"]=='') {
setcookie("SESSION", 'AUTH', 0,'/');
// 身份丢失(超时或注销)
} else if ($_SERVER['PHP_AUTH_USER']!='' && $_COOKIE["USER_SESSION"]=='') {
$_SERVER['PHP_AUTH_USER'] = '';
}
// 数据库身份验证
$ident = executeSQL("SELECT * FROM UTILISATEURS WHERE upper(IDENTIFIANT)=Upper('".$_SERVER['PHP_AUTH_USER']."')");
if ($_SERVER['PHP_AUTH_USER']!='' && strtoupper($ident['IDENTIFIANT'])==strtoupper($_SERVER['PHP_AUTH_USER']) && $ident['MOT_DE_PASSE']==$_SERVER['PHP_AUTH_PW']) {
$user = $_SERVER['PHP_AUTH_USER'];
setcookie("USER_SESSION", $user, time()+300,'/'); // 5分钟!
// 密码不正确:请求身份验证
} else {
setcookie("SESSION", '', 1,'/');
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
die;
}
?>
<html>
<body >
您好 <?php echo $ident['NOM'].' '.$ident['PRENOM']; ?>
<br>
<br>
<a href="http://logout.php">注销</a>
</body>
</html>
logout.php
<?php
setcookie("USER_SESSION", '', 1,'/');
header('Location: http://login.php');
?>
这是一个极其简单的成功注销方法。
<?php
if ( $realm == '' )
$realm = mt_rand( 1, 1000000000 );
header( 'WWW-Authenticate: Basic realm='.$realm );
?>
要注销用户,只需更改$realm的值
回到在CGI模式下进行身份验证的问题……mcbethh建议使用此方法在php中设置局部变量
RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization},L]
它不起作用。我看不到变量。我的解决方案非常迂回,但它有效
RewriteEngine on
RewriteCond %{HTTP:Authorization} !^$
RewriteCond %{REQUEST_METHOD} =GET
RewriteCond %{QUERY_STRING} =""
RewriteRule ^page.php$ page.php?login=%{HTTP:Authorization}$1
如果没有任何参数并且是GET请求,这会导致Auth字符串添加到URL中。这可以防止POST和参数列表被破坏。
然后,在PHP脚本中,我将Auth字符串存储为会话cookie。
因此,登录我的脚本的唯一方法是访问没有任何参数的URL。