一、什么是摘要认证
摘要认证,即 Digest Access Authentication,是一种HTTP身份验证机制,用于验证用户的身份。相较于基本认证(Basic Authentication)使用用户名密码的方式,提供了更高的安全性和灵活性。
二、工作流程
HTTP Digest Access Authentication 的工作流程如下:
- 客户端发送一个未经认证的请求到服务器
- 服务器返回一个 HTTP 401 Unauthorized 响应,其中包含一个 “WWW-Authenticate” 头部字段,用来表示所使用的认证方式(通常是 Digest),以及一些额外的参数,如 realm(领域)、nonce(随机数)等
- 客户端收到 401 响应后,会根据服务器提供的信息,计算出一个摘要(digest)。客户端将摘要信息添加到请求中的 “Authorization” 头部字段中,重新发送请求到服务器
- 服务器收到带有摘要的请求后,会使用相同的算法计算出一个期望的摘要,并与客户端提供的摘要进行比较。如果两者一致,则服务器会接受该请求,返回请求的资源;否则,服务器拒绝请求,可能返回 401 或其他适当的响应
三、实例演示
客户端调用服务器 API 发送请求:
class CHttpRequest : public QObject {
Q_OBJECT
public:
int sendFile(const QString &strUrl, QString &strFile, QString &strAuth, QString &recvMessage) {
m_bNeedAuth = false;
disconnect(m_pNetworkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(slot_requestFinished(QNetworkReply *))); // 请求完成信号
connect(m_pNetworkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(slot_requestRet_Syn(QNetworkReply *)));
// 构造请求头
QNetworkRequest netRequest(strUrl);
QString boundary = "----WebKitFormBoundaryyYCL2Hd3ZwCR4KhI";
QString contenType = "multipart/form-data; boundary=" + boundary;
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, contenType);
if (strAuth != "") {
netRequest.setRawHeader("Authorization", strAuth.toLatin1());
}
// 打开文件
QFile file(strFile);
if (!file.open(QIODevice::ReadOnly)) {
return -1;
}
// 构建请求数据
strFile.remove(0, strFile.lastIndexOf('/') + 1);
QString text = "Content-Disposition: form-data; name=\"fimage\"; filename=\"" + strFile + "\"\r\n";
QByteArray data;
data.append("--" + boundary + "\r\n");
data.append(text);
data.append("Content-Type: application/octet-stream\r\n\r\n");
data.append(file.readAll());
data.append("\r\n--" + boundary + "--\r\n");
// 发送POST请求
m_pNetworkReply = m_pNetworkManager->post(netRequest, data);
// ...省略部分代码
recvMessage = this->m_strRet;
if (m_bNeedAuth) {
return 401;
}
return m_loop_flag;
}
public slots:
// 接收服务器返回信息
void slot_requestRet_Syn(QNetworkReply *reply) {
// ...省略部分代码
QByteArray resultContent = reply->readAll();
QTextCodec *pCodec = QTextCodec::codecForName("UTF-8");
QString strResult = pCodec->toUnicode(resultContent);
int nHttpCode = reply>attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // http返回码
if (nHttpCode == 200) { // 请求成功
} else if (nHttpCode == 401) { // 请求失败,想要认证
// 获取服务器返回的WWW-Authenticate认证信息
QList<QByteArray> headers = reply->rawHeaderList();
for (QList<QByteArray>::const_iterator iter = headers.constBegin(); iter != headers.constEnd(); ++iter) {
qDebug() << *iter << ":" << reply->rawHeader(*iter);
QString msg = *iter;
if (msg.contains("WWW-Authenticate")) {
strResult = reply->rawHeader(*iter);
}
}
} else { // 其他错误
}
}
public:
// 计算验证信息的函数,这里的计算规则是我这台服务的规定,大家处理的时候要根据自己服务器的规则去处理
QString reAuthenticate(QString &strUrl, QString &strUser, QString &strPsw, QString &httpHeadFromServer) {
QString realm, qop, nonce, opaque;
int lastIndex = httpHeadFromServer.lastIndexOf("Digest realm=");
httpHeadFromServer = httpHeadFromServer.mid(lastIndex);
realm = httpHeadFromServer.section("realm=\"", 1, 1).split('"').first();
qop = httpHeadFromServer.section("qop=\"", 1, 1).split('"').first();
nonce = httpHeadFromServer.section("nonce=\"", 1, 1).split('"').first();
opaque = httpHeadFromServer.section("opaque=\"", 1, 1).split('"').first();
QString A1 = strUser + ":" + realm + ":" + strPsw;
QByteArray hashedA1 = QCryptographicHash::hash(A1.toUtf8(), QCryptographicHash::Sha256);
QString strHashA1 = hashedA1.toHex();
QString A2 = "POST:" + strUrl;
QByteArray hashedA2 = QCryptographicHash::hash(A2.toUtf8(), QCryptographicHash::Sha256);
QString strHashA2 = hashedA2.toHex();
QString response = strHashA1 + ":" + nonce + ":00000001:b985236c7eb52970:auth:" + strHashA2;
QByteArray hashedRes = QCryptographicHash::hash(response.toUtf8(), QCryptographicHash::Sha256);
QString strRes = hashedRes.toHex();
QString strAuth = " Digest username=\"" + strUser + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" +
strUrl + "\", algorithm=SHA-256, response=\"" + strRes + "\", opaque=\"" + opaque + "\", qop=" + qop +
", nc=00000001, cnonce=\"b985236c7eb52970\"";
return strAuth; // 处理好的认证信息
}
};
int main() {
QString strAuth = "";
CHttpRequest checkRequest;
QString strURL = "http://172.16.26.165/setup/system/update.php?app=set";
int ret = checkRequest.sendFile(strURL, strTmpFile, strAuth, strRecv); // 第一次调用API时,认证信息strAuth是空的
if (ret == 401) { // 服务器返回401
QString url = "/setup/system/update.php?app=set";
QString user = "admin";
QString psw = "12345";
strAuth = checkRequest.reAuthenticate(url, user, psw, strRecv); // 根据服务器定的规则,计算验证信息
// 使用计算出来的验证信息,重新请求API
CHttpRequest checkRequest2;
if (checkRequest2.sendFile(strURL, strTmpFile, strAuth, strRecv) != -1) {
emit msgUpgradeSta(this, Upgrade_Success);
} else {
emit msgUpgradeSta(this, Upgrade_Fail);
}
return 0;
}
return 0;
}
如上代码,客户端想通过 HTTP POST 请求,发送一个文件给服务器。第一次调用 API 的时候,没有带上验证信息 Authorization,服务器返回 401 错误,并且在返回的 HTTP 头部信息中带有 WWW-Authenticate 认证信息:
HTTP/1.1 401 Unauthorized
Set-Cookie: PHPSESSID=984d2f37f1611d4848518ca643ccbfa0; path=/; HttpOnly
Set-Cookie: PHPSESSID=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
WWW-Authenticate: Digest realm="N5M-6S335",algorithm="MD5",qop="auth",nonce="6444da9264a78",opaque="51d97021ccdf09ef7a8da27e8193cabf"
WWW-Authenticate: Digest realm="N5M-6S335",algorithm="SHA-256",qop="auth",nonce="6444da9264a78",opaque="51d97021ccdf09ef7a8da27e8193cabf"
Content-type: text/html; charset=UTF-8
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Content-Length: 0
Date: Sun, 23 Apr 2023 07:13:21 GMT
Server: WintenDolighttpd/1.4.67
在 slot_requestRet_Syn 函数中截取 WWW-Authenticate 的内容(可以看到有两种加密方式,MD5 和 SHA-256)。获取到需要的验证信息后,main 函数调用 reAuthenticate 函数,根据服务器定的规则进行验证信息的处理,这里选择 SHA-256 的加密方式,所以代码里截取出WWW-Authenticate: Digest realm="N5M-6S335",algorithm="SHA-256",qop="auth",nonce="6444da9264a78",opaque="51d97021ccdf09ef7a8da27e8193cabf
这一行信息,再分别截取出 realm、qop 等等这些字段,根据规则进行 SHA-256 加密,最终得出认证信息。最后,main 函数重新发送 HTTP 请求,在请求头的 Authorization 字段加上这串验证信息:
这样,就可以成功请求服务器了。文中用到的 HTTP 请求类,可以到点击这里下载。