本文目录导读:

在 PHP 中实现 HTTP 摘要认证(Digest Authentication) 比基本认证更安全,因为它不会明文传输密码。
PHP 本身提供了清晰的实现方式,主要通过检查 $_SERVER['PHP_AUTH_DIGEST'] 变量,并结合 MD5 哈希计算来完成验证。
以下是一个完整的、可直接运行的实现步骤和代码示例。
核心步骤
- 发送 401 状态码:当用户未认证或认证失败时,服务器返回
401 Unauthorized。 - 生成 Nonce(一次性随机数):防止重放攻击的数字。
- 发送
WWW-Authenticate头:告诉浏览器使用摘要认证。 - 接收客户端响应:浏览器会发送
Authorization头,PHP 将其存入$_SERVER['PHP_AUTH_DIGEST']。 - 解析并验证:解析该字符串,比对服务器端计算的 hash 值与客户端发来的 hash 值。
完整代码示例
<?php
// 1. 模拟的用户数据(实际应查数据库)
$users = [
'admin' => '123456', // 明文密码,实际应存哈希,但摘要认证需要原始密码计算
'user1' => 'password'
];
// 2. 检查是否收到了Authorization头(摘要认证的响应)
if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
// 未认证 -> 发送挑战(Challenge)
$realm = 'Restricted Area'; // 领域名称,浏览器会显示
$nonce = md5(uniqid(mt_rand(), true)); // 生成nonce
$opaque = md5($realm); // opaque可选,用于客户端计算
// 发送401状态码和WWW-Authenticate头
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="' . $realm .
'", qop="auth", nonce="' . $nonce .
'", opaque="' . $opaque . '"');
echo '认证被取消或失败。'; // 用户点击取消时显示
exit;
} else {
// 3. 有认证信息,开始验证
// 解析浏览器发来的 Authorization 头
$authData = parseDigestAuth($_SERVER['PHP_AUTH_DIGEST']);
if (!$authData) {
header('HTTP/1.1 401 Unauthorized');
exit('Invalid authorization header.');
}
// 检查用户名是否存在
if (!isset($users[$authData['username']])) {
header('HTTP/1.1 401 Unauthorized');
exit('Username not found.');
}
// 获取该用户的密码
$password = $users[$authData['username']];
$realm = 'Restricted Area';
// 4. 服务器端计算应有的 response 值
// 公式:HA1 = MD5(username:realm:password)
$A1 = md5($authData['username'] . ':' . $realm . ':' . $password);
// HA2 = MD5(method:uri)
$method = $_SERVER['REQUEST_METHOD'];
$A2 = md5($method . ':' . $authData['uri']);
// response = MD5(HA1:nonce:nc:cnonce:qop:HA2)
$validResponse = md5($A1 . ':' . $authData['nonce'] . ':' .
$authData['nc'] . ':' .
$authData['cnonce'] . ':' .
$authData['qop'] . ':' . $A2);
// 5. 比较客户端发来的 response 和我们计算的 validResponse
if ($authData['response'] === $validResponse) {
// 认证成功
echo '欢迎, ' . htmlspecialchars($authData['username']) . '!你已通过摘要认证。';
// 这里可以开始你的业务逻辑
} else {
// 密码错误
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="' . $realm .
'", qop="auth", nonce="' . md5(uniqid()) .
'", opaque="' . md5($realm) . '"');
exit('认证失败,密码错误。');
}
}
/**
* 解析 HTTP Digest 认证的 Authorization 头字符串
* @param string $header Authorization 头的值
* @return array|false 解析后的关联数组,失败返回 false
*/
function parseDigestAuth($header) {
// 确保是 Digest 认证
if (strpos($header, 'Digest ') !== 0) {
return false;
}
$header = substr($header, 7); // 去掉 "Digest "
$needed_parts = [
'username', 'realm', 'nonce', 'uri',
'qop', 'response', 'nc', 'cnonce'
];
$data = [];
// 使用正则提取键值对(处理引号和逗号)
preg_match_all('/(\w+)=["\']?([^"\',\s]+)["\']?,?\s*/', $header, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
$data[$m[1]] = $m[2];
}
// 检查必要的字段是否都存在
foreach ($needed_parts as $part) {
if (!isset($data[$part])) {
return false;
}
}
return $data;
}
?>
关键点解释
-
$_SERVER['PHP_AUTH_DIGEST']- 这是 PHP 自动解析
Authorization: Digest ...头后提供的变量,注意不是PHP_AUTH_USER(那是基本认证用的)。 - 如果用户没有输入或点击取消,该变量为空。
- 这是 PHP 自动解析
-
parseDigestAuth()函数- 浏览器发来的认证字符串是类似
username="admin", realm="...", nonce="...", response="..."这样的格式。 - 该函数使用正则将其解析为关联数组。
- 浏览器发来的认证字符串是类似
-
response的计算(最核心的安全逻辑)- 客户端发来
response字段,服务器需要验证它是否正确。 - 计算公式(RFC 2617):
HA1 = MD5(username:realm:password) HA2 = MD5(method:uri) response = MD5(HA1:nonce:nc:cnonce:qop:HA2) nc(nonce count):请求计数器,防重放。cnonce(client nonce):客户端生成的随机数。
- 客户端发来
-
密码存储
- 这是关键:摘要认证需要密码的 原始明文 或
HA1的值(MD5(username:realm:password))才能完成计算。 - 如果你库中存的是普通 MD5 或 bcrypt 哈希,无法 直接用于摘要认证(除非你同时存储了 HA1),通常为了提高安全性,建议使用
openssl、sodium或在 HTTPS 下使用 Token/Bearer 认证 替代。
- 这是关键:摘要认证需要密码的 原始明文 或
如何测试
- 将上述代码保存为
digest.php。 - 放到支持 PHP 的 Web 服务器(如 Apache, Nginx 或 PHP内置服务器
php -S localhost:8000)。 - 用浏览器访问
http://localhost:8000/digest.php。 - 浏览器会弹出一个登录框:
- 用户名:
admin - 密码:
123456
- 用户名:
- 输入正确则看到欢迎信息,错误则再次弹出登录框。
注意事项
- HTTPS 是必须的:摘要认证防重放但仍有中间人攻击风险,始终使用 HTTPS。
realm字段:应该与服务器配置一致,否则客户端可能不发送认证头。qop="auth":是常用选项,支持完整性保护。- 默认情况下 Apache/Nginx 可能干扰:使用 FastCGI (PHP-FPM) 时,有些配置可能导致
PHP_AUTH_DIGEST未设置,如果遇到问题,检查.htaccess或 Nginx 配置是否传递了认证头。
Nginx 配置示例(如果需要):
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param PHP_AUTH_DIGEST $http_authorization;
# ... 其他配置
}
认证的核心在于:
- 正确处理 WWW-Authenticate 和
$_SERVER['PHP_AUTH_DIGEST']。 - 严格按照 MD5 组合公式 验证客户端发来的
response。 - 注意密码存储方式与摘要认证的兼容性。
如果希望更简单、更现代的方式,推荐使用 Bearer Token (JWT) 结合 HTTPS,比摘要认证更灵活且易于扩展。