HTTP 认证方式的不完整总结
这篇文章最后更新的时间在六个月之前,文章所叙述的内容可能已经失效,请谨慎参考!
基本认证 (Basic Authentication)
当浏览器请求需要认证的资源时,服务端返回 401 状态码,并在响应头里加上一个字段 WWW-Authenticate , WWW-Authenticate 里有两个值 Basic 表示认证的方式, realm 是安全域字符串(一显示在浏览器的登录框里的提示,但现在的浏览器好像都不显示这个了)。
例如这样
HTTP/1.1 401 Unauthorized
Date: Wed, 21 Oct 2015 07:28:00 GMT
WWW-Authenticate: Basic realm="Access to staging site"
浏览器在遇到 401 时,一般会弹出一个输入框输入账号密码,然后再发送一次包含账号密码的请求。
账号密码会以中间一个冒号 (:) 的形式拼接,例如这样
username:password
然后再经过 basd64 编码
echo -n "username:password" | base64
# echo 里的 -n 参数是为了行尾不输出换行符
最后把结果放在请求头的 Authorization 字段里,例如这样
GET / HTTP/1.1
Host 127.0.0.1
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
认证失败时,服务器可以返回 403 或 401 。
基础认证可以把账号密码加在 url 里,这样就不会弹出询问框了,但这不是一种安全的形式,据说新版的 Chrome 已经禁用了这种形式。
https://username:password@www.example.com/
退出登录时,只需要服务器响应一个 401 或用 ajax 故意发送一个错误的账号密码 或者 在浏览器的地址栏故意输入错误的账号密码(但下次登录时要记得检查url里有没有错误的账号) 就可以的了。
基础认证最好配合 HTTPS ,如果没有 HTTPS 只要随便抓个包就能知道账号密码。
用于退出登录的 js
// 用这样的方式退出登录,需要刷新一次才可以重新登录,不然会保留错误的用户名导致一致登录失败
(function() {
var xmlhttp;
if (window.XMLHttpRequest) xmlhttp = new XMLHttpRequest();
else xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4) {
location.reload();
}
}
xmlhttp.open("GET", location.origin, true);
xmlhttp.setRequestHeader("Authorization", "Basic YXNkc2E6");
xmlhttp.send();
return false;
})()
基本认证的 apache 配置
# Apache/2.4.62
# mod_auth_basic mod_auth_digest mod_authn_file mod_authn_core mod_authz_core mod_authz_user 需要启用这几个模块
<VirtualHost localhost-auth-basic.com:80>
ServerAdmin webmaster@dummy-host.example.com
DocumentRoot "${SRVROOT}/htdocs"
ServerName localhost-auth-basic.com
ServerAlias localhost-auth-basic.com
<Location "/">
# 禁止使用 .htaccess 文件来覆盖当前目录下的 Apache 设置
AllowOverride None
# Indexes:当没有 index.html 等默认文件时显示目录列表。FollowSymLinks:允许跟随符号链接。
Options Indexes FollowSymLinks
# 基本认证对话框中显示的提示信息
AuthName "用户名"
# 认证类型为 HTTP Basic 认证
AuthType Basic
# 指定用户密码文件的位置
# 这几项都是有效的,如果填相对目录,那么是相对服务器根目录
# AuthUserFile .passwd
# AuthUserFile "${SRVROOT}/.passwd"
# AuthUserFile "/etc/apache2/.passwd"
AuthUserFile "./.passwd"
# AuthBasicFake demo demopass
# 要求访问者必须提供有效的用户名和密码
Require valid-user
</Location>
<IfModule dir_module>
DirectoryIndex index.html index.php
</IfModule>
ErrorLog "logs/localhost-auth-basic.com-error.log"
CustomLog "logs/localhost-auth-basic.com-access.log" common
</VirtualHost>
htpasswd 的使用例子
创建用户文件并添加第一个用户,如果文件已存在会覆盖已存在的文件
htpasswd -c -B .passwd user1
添加新用户
htpasswd /path/to/.htpasswd username
修改用户密码
htpasswd /path/to/.htpasswd username
使用 bcrypt 加密
htpasswd -B /path/to/.htpasswd username
删除用户
htpasswd -D /path/to/.htpasswd username
用 php 实现的基本认证例子
$userLists = [
'qwe' => '123',
];
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="git auth"');
header('HTTP/1.0 401 Unauthorized');
echo 'Authorization Required.';
exit;
}
$PHP_AUTH_USER = $_SERVER['PHP_AUTH_USER'] ?? '';
$PHP_AUTH_PW = $_SERVER['PHP_AUTH_PW'] ?? '';
if (!isset($userList[$PHP_AUTH_USER]) || $userList[$PHP_AUTH_USER] != $PHP_AUTH_PW) {
header('HTTP/1.0 403 Forbidden');
echo '403 Forbidden';
exit;
}
if (isset($_GET['logout'])) {
header('HTTP/1.0 401 Unauthorized');
echo 'logout';
exit;
}
echo 'auth success';
echo '<pre>';
var_dump($_SERVER);
echo '</pre>';
参考 https://www.php.net/manual/zh/features.http-auth.php
摘要认证 (Digest Access Authentication)
在浏览器里的表现形式和基础认证基本一致,但发送账号密码的部分不一样,密码包含在数字摘要里,这样可以避免被抓包时就直接知道密码。
摘要认证无法防止中间人攻击。一个中间人可以告知客户端使用基本认证模式,而摘要访问认证没有提供任何机制帮助客户端验证服务器的身份。
服务端响应头里的 WWW-Authenticate
realm | 安全域字符串 |
qop | 保护质量,可以取值 auth 和 auth-int ,一般用 auth |
nonce | 随机字符串 |
opaque | 穿透字符串,客户端会不改变地返回给服务端 |
algorithm | 摘要算法,默认值是 MD5 |
客户端请求头里的 Authenticate
realm | 和请求头里的一致 |
qop | 和请求头里的一致 |
nonce | 和请求头里的一致 |
opaque | 和请求头里的一致 |
algorithm | 和请求头里的一致 |
username | 用户名 |
nc | 认证的次数 |
cnonce | 客户端产生的一个 GUID |
uri | uri |
response | 摘要 |
摘要的计算,其中 H 表示的是摘要算法
H(H(A1):nonce:nc:cnonce:qop:H(A2))
A1
username:realm:passwd
A2
当 qop 是 auth 时, A2 只包含请求方法和 uri , 请求方法是大写的
method:uri
当 qop 是 auth 时, A2 只包含请求方法和 uri , 还添加了报文主体部分
method:uri:H(entity-body)
摘要认证的 apache 配置
<VirtualHost localhost-auth-digest.com:80>
ServerAdmin webmaster@dummy-host.example.com
DocumentRoot "${SRVROOT}/htdocs"
ServerName localhost-auth-digest.com
ServerAlias localhost-auth-digest.com
# 这一段放在 <Directory /var/www/> 也可以
<Location "/">
# 禁止使用 .htaccess 文件来覆盖当前目录下的 Apache 设置
AllowOverride None
# Indexes:当没有 index.html 等默认文件时显示目录列表。FollowSymLinks:允许跟随符号链接。
Options Indexes FollowSymLinks
# 基本认证对话框中显示的提示信息 这里的 AuthName 必须和密码文件里的 realm 一致,这里只能填英文
AuthName "username"
# 认证类型为 HTTP Digest 认证
# "http://localhost-auth-digest.com/"
AuthType Digest
AuthDigestDomain "/"
AuthDigestProvider file
AuthUserFile "./.digest_pw"
# 要求访问者必须提供有效的用户名和密码
Require valid-user
</Location>
<IfModule dir_module>
DirectoryIndex index.html index.php
</IfModule>
ErrorLog "logs/localhost-auth-digest.com-error.log"
CustomLog "logs/localhost-auth-digest.com-access.log" common
</VirtualHost>
htdigest 的使用例子
创建用户文件并添加第一个用户,如果文件已存在会覆盖已存在的文件
htdigest -c 文件路径 realm 用户名
htdigest -c .digest_pw "private area" user2
添加新用户
htdigest .digest_pw "private area" user2
修改用户密码
htdigest .digest_pw "private area" user2
htdigest 好像没有删除用户的命令,但直接改文件也不是不可以的。。。
htpasswd 和 htdigest 都可以在 bin 目录下找到, htpasswd 和 htdigest 生成的都是文本文件
用 php 实现的摘要认证例子
$userLists = [
'qwe' => '123',
];
$realm = 'digest.test';
$nonce = md5(uniqid());
$opaque = md5($nonce . $realm);
$digest = [
'realm' => $realm, // 显示在浏览器的登录框里的提示,但现在的浏览器好像都不显示这个了
'qop' => 'auth', // auth 表示只进行身份查验 auth-int 表示进行查验外,还有一些完整性保护。需要看更详细的描述,请参阅RFC2617
'nonce' => $nonce, // 随机数
'opaque' => $opaque, // 穿透参数
'algorithm' => 'MD5', // 摘要算法,默认是 MD5
];
$a = join(', ', array_map(function ($k, $v) {
return $k . '="' . $v . '"';
}, array_keys($digest), $digest));
if (!isset($_SERVER['PHP_AUTH_DIGEST'])) {
header('HTTP/1.0 401 Unauthorized');
header('WWW-Authenticate: Digest ' . $a);
echo 'Authorization Required.';
exit;
}
if (isset($_GET['logout'])) {
header('HTTP/1.0 401 Unauthorized');
echo 'logout';
exit;
}
$digest = $_SERVER['PHP_AUTH_DIGEST'];
$digestInfo = [
'realm' => '',
'qop' => '',
'nonce' => '',
'opaque' => '',
'algorithm' => '',
'username' => '', // 用户名
'nc' => '', // 认证的次数
'cnonce' => '', // 客户端产生的一个 GUID
'uri' => '', // uri 就是请求行里的 uri
'response' => '', // 摘要
];
foreach (explode(', ', $digest) as $item) {
preg_match('/(?<key>.+)=(?<value>.*)/', $item, $match);
if (!empty($match['key'])) {
$digestInfo[$match['key']] = trim($match['value'], '\"');
}
}
echo '<pre>';
var_dump($_SERVER);
var_dump($digestInfo);
echo '</pre>';
if (!isset($userLists[$digestInfo['username']])) {
header('HTTP/1.0 401 Unauthorized');
header('WWW-Authenticate: Digest ' . $a);
echo 'username is invalid ';
exit;
}
if ($digestInfo['opaque'] != md5($digestInfo['nonce'] . $realm)) {
header('HTTP/1.0 401 Unauthorized');
header('WWW-Authenticate: Digest ' . $a);
echo 'opaque is invalid ';
exit;
}
$A1 = $digestInfo['username'] . ':' . $realm . ':' . $userLists[$digestInfo['username']];
$A2 = $_SERVER['REQUEST_METHOD'] . ':' . $digestInfo['uri'];
if (md5(implode(':', [
md5($A1),
$digestInfo['nonce'],
$digestInfo['nc'],
$digestInfo['cnonce'],
$digestInfo['qop'],
md5($A2)
])) != $digestInfo['response']) {
header('HTTP/1.0 401 Unauthorized');
header('WWW-Authenticate: Digest ' . $a);
echo 'auth fail';
exit;
}
echo 'auth success';
表单认证
表单认证大概就是最常用的认证方式了。
这是一个简单的表单认证例子
<form action="demo-form.php">
username: <input type="text" name="username"><br>
Password: <input type="password" name="password"><br>
<input type="submit">
</form>
表单认证讨论得最多得问题是,会话状态要保存在哪里 cookie? token? jwt? session? 本文只描述 http 的认证,这个问题就不展开了。
SSL 客户端证书
SSL 客户端证书,应该是最安全的形式了,但也是最麻烦的形式,网站必须启用 https 和 要在客户端安装一个证书。
除此之外,客户端证书还要单独向 CA 申请。如果是内部自建的 CA 还好,不然申请客户端证书还会有额外的成本。
客户端证书实际上就是 CA 颁发一个 CN 是 username 的证书。
总结
基础认证和摘要认证都不够安全,网站要加上 https 。
几种认证方式可以组合来使用, 基础认证/摘要认证 + 表单认证 + SSL 客户端证书 。
http 的认证还有很多种
Bearer |
HOBA |
Mutual |
OAuth |
SCRAM-SHA-1 |
SCRAM-SHA-256 |
vapid |
除此之外还有一些没有标准化的认证方式
SPNEGO |
Kerberos |
NTLM |
Negotiate |
参考
http 认证 https://www.rfc-editor.org/rfc/rfc7235.html
http 摘要认证 https://www.rfc-editor.org/rfc/rfc7616.html
http 基础认证 https://www.rfc-editor.org/rfc/rfc7617.html
MDN 关于 http 认证的描述 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Authentication
Authentication Scheme https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml
https://hc.apache.org/httpcomponents-client-4.5.x/current/tutorial/html/authentication.html