如果你有一个简单的页面需要做登录认证,也就是需要一个登录框,但是自己又懒得写一堆HTML和CSS,那么你可以借用浏览器的HTTP认证。

我们可以用PHP的 header() 函数来向客户端浏览器发送401认证信息,使浏览器弹出一个用户名/密码输入窗口。当用户输入用户名和密码提交后,浏览器将用户名和密码转码后添加到HTTP请求头发送给服务器,而PHP脚本将接收到用户名和密码存储到两个变量里:$_SERVER['PHP_AUTH_USER']、$_SERVER['PHP_AUTH_PW'] 。

浏览器支持两种常见的认证方法,分别是“Basic”和“Digest”。


Basic认证

Basic认证代码举例:

<?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请求头多了一行 “Authorization” :

其中Basic指明了使用认证方式(首字母必须大写),接着1个空格,后面这一串字符“YWRtaW46YWExMjM0NTY=”则是我们输入的“用户名:密码”经过base64转码后的字符串(中间有个冒号)。

一旦认证成功,后续的每个HTTP请求都会携带这个“Authorization”请求头。

如何注销登录呢?方法一:关闭浏览器后自动销毁(类似于session);方法二:重新向浏览器发送401状态(上面那两行header代码),浏览器会立即清除认证缓存。

Basic认证方法的认证过程简单明了,适合于对安全性要求不高的系统或设备中,如大家所用路由器的配置页面的认证,几乎都采取了这种方式。其缺点是没有灵活可靠的认证策略,如无法提供域(domain或realm)认证功能,另外,BASE64的随便解密,安全性无保障。当然,Basic认证系统也可以与SSL或者Kerberos结合,实现安全性能较高(相对)的认证系统。


Digest认证

Digest认证代码举例:

<?php
$realm = 'Restricted area';

// 用户名 => 密码
$users = [
    'admin' => 'mypass',
    'jack'  => '123456'
];


if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
    $nonce = uniqid();
    $opaque = md5($realm);
    header('HTTP/1.1 401 Unauthorized');
    header('WWW-Authenticate: Digest realm="'.$realm.'", qop="auth", nonce="'.$nonce.'", opaque="'.$opaque.'"');
    die('如果用户点击了“取消”按钮将会看到这段文字');
}


// 分析PHP_AUTH_DIGEST变量
if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || !isset($users[$data['username']])) {
    die('认证失败');
}

// 验证请求是否有效
$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('认证失败!');
}

// 到这里表示认证成功
echo '您登录的账号是:'.$data['username'];


// 用于解析 http auth 请求头的函数
function http_digest_parse($txt) {
    // 防止丢失数据
    $needed_parts = [
        'nonce'    => 1,
        'nc'       => 1,
        'cnonce'   => 1,
        'qop'      => 1,
        'username' => 1,
        'uri'      => 1,
        'response' => 1
    ];
    $data = [];

    preg_match_all('@(\w+)=([\'"]?)([a-zA-Z0-9=./\_-]+)\2@', $txt, $matches, PREG_SET_ORDER);

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

    return $needed_parts ? false : $data;
}
?>

这次 header() 函数发送的数据多了几个参数,具体含义解释如下:

  • Digest:认证方法;
  • realm:领域,领域参数是强制的,在所有的盘问中都必须有,它的目的是鉴别SIP消息中的机密,在SIP实际应用中,它通常设置为SIP代理服务器所负责的域名;
  • qop:保护的质量,这个参数规定服务器支持哪种保护方案,客户端可以从列表中选择一个。值 “auth”表示只进行身份查验, “auth-int”表示进行查验外,还有一些完整性保护。需要看更详细的描述,请参阅RFC2617
  • nonce:为一串随机值,在下面的请求中会一直使用到,当过了存活期后服务端将刷新生成一个新的nonce值;
  • opaque:一个不透明的(不让外人知道其意义)数据字符串,在盘问中发送给用户。

 

登录框还是一个样子,但这次的HTTP请求头不太一样:

太长了,我贴出来:

Authorization: Digest username="admin", realm="Restricted area", nonce="5c829a8c92474", uri="/auth2.php", response="b570c5fdd02e997de9a5f5582f447447", opaque="cdce8a5c95a1427d74df7acbf41c9ce0", qop=auth, nc=00000002, cnonce="52c83b15aa2d298b"

其中,参数realm、nonce、opaque、qop是由上一个请求服务器返回给客户端的数据。客户端新生成的参数含义如下:

  • username:登录用户名(明文);
  • uri:客户端想要访问的URI;
  • response:这是由用户代理软件计算出的一个字符串,以证明用户知道口令;
  • nc:“现时”计数器,这是一个16进制的数值,即客户端发送出请求的数量(包括当前这个请求),这些请求都使用了当前请求中这个“现时”值。例如,对一个给定的“现时”值,在响应的第一个请求中,客户端将发送“nc=00000001”。这个指示值的目的,是让服务器保持这个计数器的一个副本,以便检测重复的请求。如果这个相同的值看到了两次,则这个请求是重复的;
  • cnonce:这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护。

PHP参考手册:http://php.net/manual/zh/features.http-auth.php
HTTP认证机制:https://www.cnblogs.com/attilax/p/6226004.html