针对 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 '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>";
}
?>
注意: 兼容性注意
在编写 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 "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: " . htmlspecialchars($_SERVER['PHP_AUTH_USER']) . "<br />";
echo "Old: " . 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='Re Authenticate' />\n";
echo "</form></p>\n";
}
?>
此行为不是 HTTP Basic
身份验证标准所必需的,因此您永远不应该依赖此行为。使用 Lynx
进行测试表明,Lynx
不会在收到 401 服务器响应时清除身份验证凭据,因此,只要凭据要求没有更改,按后退键,然后再次按向前键将打开资源。但是,用户可以按 '_'
键清除其身份验证信息。
要使 HTTP 身份验证在使用 PHP 的 CGI 版本的 IIS 服务器上正常工作,您必须编辑 IIS 配置 "Directory Security
"。单击 "Edit
",然后仅选中 "Anonymous Access
",所有其他字段应保持未选中状态。
注意: IIS 注意:
要使 HTTP 身份验证在 IIS 上正常工作,PHP 指令 cgi.rfc2616_headers 必须设置为0
(默认值)。
针对 CGI/FastCGI Apache 下缺少 Authorization 头部的解决方法
SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
现在,如果客户端发送 Authorization 头部,PHP 应该会自动声明 $_SERVER[PHP_AUTH_*] 变量。
对于使用 CGI 的 PHP,请确保将重写规则放在您可能拥有的任何其他重写规则之上。
在我的情况下,我将它放在 .htaccess 的顶部(在 RewriteEngine On 下面)
RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization}]
我的症状是 REMOTE_USER(或我的情况下的 REDIRECT_REMOTE_USER)根本没有被设置。
原因:我有一些其他 RewriteRule 正在起作用,并且设置为 LAST 规则。
希望这有帮助。
这是我发现的最简单的使用重试进行基本身份验证的形式。
<?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 ("Not authorized");
}
// 如果到达这里,则为有效用户。
echo "<p>Welcome $user.</p>";
echo "<p>Congratulation, you are into the system.</p>";
?>
在 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 访问它。
不要忘记从您的 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 头部的函数
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;
}
?>
这是我尝试创建一个摘要身份验证类,它将登录和注销用户,而无需使用 cookie、会话、数据库或文件。核心是这段解析摘要字符串到变量的简单代码,适用于多个浏览器。
<?php
// 使用多浏览器支持的 explode digest(由 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 登录。转到第二页获取代码链接。欢迎提出意见、想法、建议或批评。
不要在 RewriteRule 中使用 "last" ("L") 指令!这会阻止所有后续的重写规则在给出 Basic 或 Digest 身份验证时被跳过,这几乎肯定不是你想要的。
因此,以下几行代码足以用于 .htaccess(或 httpd.conf)文件
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
我想出了另一种方法来解决浏览器缓存 WWW 身份验证凭据并导致注销问题的问题。虽然大多数浏览器都有一些方法可以清除这些信息,但我更喜欢让我的网站来处理这项任务,而不是依赖用户的理智。
即使使用 Lalit 的方法来创建随机的 realm 名称,仍然可以通过在 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_AUTH_USER 和 PHP_AUTH_PW 服务器变量问题的另一个 "补丁",该问题发生在将 PHP 作为 CGI 运行时。
首先不要忘记在你的 .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 编码的字符串,丢弃前 6 个字符 "Basic ",然后进行常规验证。
在脚本末尾,我们打印变量以验证其是否正常工作。在生产版本中应省略此操作。
这是 Bernard Paques 编写的脚本的变体。
感谢他提供了这段代码。
请注意,Microsoft 发布了一个“安全更新”,该更新禁用了在 http url 中使用用户名:密码@主机。
http://support.microsoft.com/default.aspx?scid=kb;en-us;834489
不幸的是,上述依赖此功能的方法将不再适用于 Microsoft 浏览器。
您可以按照以下说明重新启用此功能:
http://weblogs.asp.net/cumpsd/archive/2004/02/07/69366.aspx
但您的用户可能不愿意这样做。
在德语示例 #2(摘要)中,<?php $realm = "Geschützter Bereich"; ?>。根据我的测试,变音字母 ü 会有问题,导致无限循环输入密码。在我的情况下,它是用 UTF-8 编写的。因此,我建议仅对 realm 使用纯 ASCII 字符。
我无法让身份验证在任何示例中正常工作。最后,我从 ZEND 的教程示例开始:
http://www.zend.com/zend/tut/authentication.php?article=authentication(使用 .htpasswd 进行验证)并尝试处理其他情况。我的总体结论是,更改 realm 是导致浏览器再次询问的唯一可靠方法,我感谢将该示例放入手册的人,因为它让我走上了正轨。无论如何,浏览器都拒绝丢弃它已经记住的值。当然,更改 realm 的问题在于,您不希望在给定会话中执行此操作,否则会导致新的密码请求。所以,这里有一个例子,希望复制粘贴不会太乱。
我花了大部分时间让它正常工作。我很难想明白浏览器在遇到身份验证请求时会怎么做:在我看来,它会尝试获取密码,然后重新加载页面……因此不会运行 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'] );
// Below here runs HTML-wise only if there isn't a $_SESSION,
// and the browser *can't* set $PHP_AUTH_USER... normally
// the browser, having gotten the auth info, runs the page
// again without getting here.
// What I'm basically getting to is that the way to get
// here is to escape past the login screen. I tried
// putting a session_destroy() here originally, but the
// problem is that the PHP runs regardless, so the
// REFRESH seems like the best way to deal with it.
echo "<meta http-equiv=\"REFRESH\"
content=\"0;url=index.php\">" ;
exit;
}
if ($_POST['logout'] == "logout") {
session_destroy() ;
header('Location: comeagain.php');
exit ;
}
// "standard" authentication code here, from the ZEND tutorial above.
comeagain.php is as follows:
<?
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>" ;
?>
这样做的目的是能够丢弃会话(从而重置 realm),而不会提示浏览器再次询问……因为它已被重定向到 logout.php。
使用这种组合,我可以让事情正常工作。只要确保不要让 apache 同时运行 htpasswd 身份验证,否则事情会变得很奇怪 :-)。
要使其与 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 上运行良好。
如果您必须在使用“http_digest_parse”函数验证响应之前使用“setlocale”函数,请谨慎使用 http 摘要身份验证(请参见上面的示例 34.2),因为“preg_match_all”函数的模式与 \w 存在冲突。
实际上,由于 \w 应该代表任何字母或数字或下划线字符,因此您必须不要忘记这可能会根据您的区域设置配置而有所不同(例如,它在法语中接受重音字母)……
由于“preg_match_all”函数对模式的不同解释,“http_digest_parse”函数在您修改了区域设置后将始终返回错误结果(我的意思是,如果您的区域设置接受某些扩展字符,请参见 http://fr.php.net/manual/en/reference.pcre.pattern.syntax.php 获取更多信息)。
IMO,我建议您不要在完成身份验证之前使用 setlocale……
PS:这是一个不兼容的 setlocale 声明……
setlocale ( LC_ALL, 'fr_FR', 'fr', 'FR', 'french', 'fra', 'france', 'French', 'fr_FR.ISO8859-1' ) ;
您好。
抱歉,我的英语不好。
此示例展示了“登录”、“注销”和“重新登录”的编程。
此脚本必须在受保护的页面中使用。
要使此脚本正常工作,浏览器地址字符串必须如下所示:
"http://localhost/admin/?login" - 登录,
"http://localhost/admin/?logout" - 注销,
"http://localhost/admin/?logout&login" - 重新登录。
<?php
session_start();
$authorized = false;
# LOGOUT
if (isset($_GET['logout']) && !isset($_GET["login"]) && isset($_SESSION['auth']))
{
$_SESSION = array();
unset($_COOKIE[session_name()]);
session_destroy();
echo "logging out...";
}
# checkup login and password
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;
}
}
# login
if (isset($_GET["login"]) && !$authorized ||
# relogin
isset($_GET["login"]) && isset($_GET["logout"]) && !isset($_SESSION['reauth']))
{
header('WWW-Authenticate: Basic Realm="Login please"');
header('HTTP/1.0 401 Unauthorized');
$_SESSION['auth'] = true;
$_SESSION['reauth'] = true;
echo "Login now or forever hold your clicks...";
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="Test auth"');
header('HTTP/1.0 401 Unauthorized');
echo 'Auth failed';
exit;
}
?>
我使用了 Louis 的示例(2006 年 6 月 3 日),它对我来说很好用(谢谢)。
但是,我添加了一些行,以确保用户只在几次尝试后才会看到身份验证窗口。
<?php
$realm = mt_rand( 1, 1000000000)."@YourCompany";
$_SESSION['realm'] = $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 'Authorization Required.';
exit;
}
} else {
echo '<P>You are authorized!</P>';
}
?>
noentry.php 与 comeagain.php 稍有不同。
在 Apache HTTP Server 2.4.13 及更高版本中,针对 CGI/FastCGI 下缺少 Authorization 标头的简单解决方法。
CGIPassAuth On
请勿使用基本身份验证启用 Authorization 标头,它非常不安全。
在 Windows 2003 Server/IIS6 中,使用 php4+ cgi,我只能使用以下方法使 HTTP 身份验证正常工作
<?php header("Status: 401 Access Denied"); ?>
with
<?php header('HTTP/1.0 401 Unauthorized'); ?>
不工作!
我还需要在“自定义错误”中选择“401;1”到“401;5”的范围,然后点击“设置为默认值”按钮。
感谢 rob at theblip dot com
某些服务器不支持 HTTP1.0 规范,会返回 500 错误(例如)。这发生在我上传身份验证脚本的服务器上。
如果发生这种情况,您可以尝试 HTTP1.1 标头语法
<?php
header("WWW-Authenticate: Basic realm=\"My Realm\"");
header('status: 401 Unauthorized');
?>
链接到 http://logout:logout@<?=$_SERVER['HTTP_HOST'];?>/SECRET/ 将强制对 /SECRET 目录进行重新登录,前提是用户没有使用密码 logout 进行注销。
[注意:danbrown AT php DOT net:以下说明由“匿名”添加于 2010 年 4 月 1 日(虽然我们认为这不是愚人节玩笑)。]
这种注销方法不再完全有效,因为 M$ 又搞出了新花样。
http://support.microsoft.com/kb/834489
有一些 .htaccess 实际上对我们(cPanel + phpsuexec)有效,即使其他方法失败了。也许它对某些人有帮助。
# PHP(CGI 模式)使用 ModRewrite 进行 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('Admin access turned off');
}
使用基本身份验证回退的 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);
}
?>
它每次访问页面时都会强制进行身份验证。
(也许可以帮助某人)
<?
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 数组
$_SESSION = array();
session_destroy();
?>
要强制使用基本身份验证注销,可以更改 Realm,让它不再是原来的 Realm。
这会强制使用新的凭据,用于服务器上的新“Realm”。
你只需要跟踪 Realm 名称和用户名/密码,并在用户登录和注销时更改为新的/随机的名称。
我相信这是唯一一种可以 100% 保证 HTTP 基本身份验证注销的方法,如果将其添加到文档中,这里很多糟糕的用户贡献评论就可以删除了。
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,"用户名和密码不匹配!");
}
}
一个简短的用户登录示例
对于任何尝试过上面摘要示例但没有成功的人。
对我来说,问题似乎是使用过时的'\'(反斜杠)在正则表达式中代替'$'(美元符号)来表示反向引用。此外,结果必须从剩余的双引号和单引号中删除。
这是一个有效的示例
// 用于解析 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
关于 tigran 在 freenet dot am 上的 php+mysql 身份验证代码
存在一些安全漏洞。
首先
$user
和
$pass
都不安全,它们可能使这段代码容易受到 SQL 注入攻击,您应该始终从两者中删除无效字符,或者至少对它们进行编码。
实际上,将密码存储为 MD5 哈希值会让您更容易保护。
第二个安全风险
同一个 mysql 用户具有更新和选择权限,甚至可能还有插入权限,更不用说您的身份验证数据库了。
同样,SQL 注入攻击可能会发生,最终用户可以更改用户的用户名、密码或与之相关的任何其他内容。
第三点更多的是性能问题。
您真的需要更新数据库吗?因为更新比选择速度慢,如果您每次访问页面时都进行更新,那么您会付出一些速度损失的代价。
一种选择是,如果您想使用 sql(我认为 mysql 有它),那就是内存数据库,并在内存中创建一个表,该表存储每个登录用户的唯一会话标识符,或者,如果它是一个单一的 前端系统,您可以使用 db 文件。
# PHP(CGI 模式)使用 ModRewrite 进行 HTTP 授权
# 最正确的示例,带有非空标头检查
RewriteEngine on
RewriteCond %{HTTP:Authorization} !^$
RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization}, \
E=PHP_AUTH_USER:%{HTTP:Authorization},L]
我认为让身份验证正确工作很容易。我使用一个会话变量来强制用户每次访问登录区域时都进行身份验证。
<?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();
// 您下面的代码
}
?>
不要在纯文本中使用 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" 查询中。
希望这有帮助 :)
我建议将用户身份验证和管理请求到 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(); // 常见的标头 WWW-Authenticate ...
fclose($hh);
关于使用 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 的方法来创建随机的 realm 名称,仍然可以通过在 Firefox 中使用后退按钮回到受保护区域,因此这方法不可行。以下是我的解决方案
由于浏览器将凭据附加到特定的 URL,因此请使用虚拟路径,其中路径的一个组件实际上是 PHP 脚本,而其后的所有内容都是 URI 的一部分,例如
http://www.example.com/some_dir/login.php/auth/8f631b92/
通过为 URL 的最后一个组件选择一个不同的数字,浏览器可能会被欺骗成认为它们正在处理一个完全不同的网站,从而再次提示用户输入凭据。
请注意,使用随机的、无限制的数字仍然允许用户点击后退按钮回到页面。您应该在服务器端文件或数据库中跟踪此数字,并在每次成功登录时重新生成它,以便最后几个数字变为无效。使用无效数字可能会导致 403 响应,或者,取决于您当天的心情,可能会导致 302 重定向到一个恶意网站。
在这种情况下,从生成的页面链接时应谨慎,因为相对链接将相对于虚拟的、不存在的目录,而不是实际的脚本目录。
希望这对某些人有所帮助。
虽然摘要身份验证仍然远远优于基本身份验证,但人们必须牢记许多安全问题。
在这方面,上面给出的摘要示例存在一些缺陷,因为 nonce 从未超时或以其他方式变为无效。因此,它成为密码等价物(尽管仅限于该特定 URL),并且可以被窃听者用来随时获取页面的最新版本,从而允许攻击者始终访问页面的最新版本,或者(更糟糕的是)重复调用 CGI 脚本 - 例如,如果用户请求 URL "/filemanager?delete=somefile",攻击者可以在将来的任何时间重复此删除操作,可能是在文件被重新创建之后。
虽然可能无法在不重新进行身份验证的情况下更改 GET 数据,但可以更改 cookie 和 POST 数据。
为了防止第一个问题,nonce 可以包含一个时间戳,并且可以添加一个检查以确保超过 30 分钟的 nonce 会导致新的身份验证请求。
为了解决第二个问题,需要生成一个一次性 nonce - 也就是说,必须拒绝使用特定 nonce 的所有进一步请求。
一种方法是:当用户请求 "deletefile" 等操作时,在会话变量中存储一个随机生成的 nonce,使用该 nonce 发出 401 身份验证挑战,然后在收到身份验证时根据存储的值进行检查(并清除会话变量)。
这样,虽然可能的窃听者收到了 nonce 并且因此获得了执行该操作的能力,但他只能执行一次 - 并且用户本来也会执行该操作。(只有用户或攻击者,但不能两者都有,可以执行该操作,因此它是安全的。)
当然,在某些时候,安全性只能通过切换到 HTTPS/SSL/TLS 来提高(例如,这是防御中间人攻击的唯一方法)。您决定安全级别。
首先,对于我的英语,我感到抱歉。
另一个带有注销解决方案的授权脚本。
在 meint_at_meint_dot_net (https://php.net/manual/en/features.http-auth.php#93859) 给出的脚本中,使用了 rewrite_module。如果该模块有任何问题,并且对 Web 服务器的管理权限受到限制(包括对使用 .htaccess 的限制),那么您可以使用此解决方案。
index.php
---------
<?php
$auth_realm = 'My 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
// 尝试模拟 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。