我需要使用Qt客户端实现SPNEGO。服务器以401 / Unauthorized响应并发送WWW-Authenticate:Negotiate标头。
答案 0 :(得分:5)
首先,通过RFC在这里了解协议:https://tools.ietf.org/html/rfc4559
另一个很棒的参考是https://github.com/requests/requests-kerberos
了解GSSAPI虽然存在跨平台实现,但在Windows上并不存在,而是您需要使用SSPI。这个答案将展示如何在Windows上实现它。您可以将功能映射到GSSAPI以用于其他平台。你永远不会真实地编写这样的代码,但是我为那些需要在没有太多抽象的情况下实现SPNEGO的人创建了这个。
此示例使用此处创建的奇妙Kerberos环境:https://github.com/Brandon-Godwin/vagrant-kerberos-environment
#include <QCoreApplication>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QtDebug>
#include <QNetworkReply>
#include <QAuthenticator>
#include <QNetworkRequest>
#include <QNetworkProxy>
#include <QNetworkCookieJar>
#include <QNetworkCookie>
#define SECURITY_WIN32
#include <windows.h>
#include <security.h>
#pragma comment(lib,"secur32.lib")
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QNetworkAccessManager manager;
manager.connect(&manager,&QNetworkAccessManager::authenticationRequired,
[](QNetworkReply * reply, QAuthenticator * authenticator) {
qDebug() << "AUTH REQUIRED" << reply << authenticator;
});
manager.connect(&manager,&QNetworkAccessManager::proxyAuthenticationRequired,
[](const QNetworkProxy & proxy, QAuthenticator * authenticator) {
qDebug() << "AUTH REQUIRED" << proxy << authenticator;
});
qDebug() << "RUNNING";
QNetworkRequest request;
request.setUrl(QUrl("http://dc.testdomain.lan:8080/hello"));
auto reply = manager.get(request);
reply->connect(reply,&QNetworkReply::finished,
[reply,request,&manager](){
reply->deleteLater();
if(reply->rawHeader("www-authenticate") == "Negotiate") {
qDebug() << reply->rawHeaderList();
qDebug() << reply->rawHeader("set-cookie");
CredHandle cred = {0};
TimeStamp exp;
qDebug() << "Acquire"
<< AcquireCredentialsHandleA(NULL,(LPSTR)"Kerberos",SECPKG_CRED_OUTBOUND,NULL,NULL,NULL,NULL,&cred,&exp);
CtxtHandle ctx;
SecBufferDesc outputBufferDesc;
SecBuffer outputBuffers[1];
outputBuffers[0].pvBuffer = NULL;
outputBuffers[0].BufferType = SECBUFFER_TOKEN;
outputBuffers[0].cbBuffer = 0;
outputBufferDesc.ulVersion = SECBUFFER_VERSION;
outputBufferDesc.cBuffers = 1;
outputBufferDesc.pBuffers = outputBuffers;
ULONG contextAttr;
auto ret = InitializeSecurityContextA(&cred,NULL,(LPSTR)"HTTP/dc.testdomain.lan:8080",ISC_REQ_ALLOCATE_MEMORY,0,
SECURITY_NATIVE_DREP,NULL,0,&ctx,&outputBufferDesc,&contextAttr,&exp);
// TODO: De-allocate outputBufferDesc.pBuffers[0]
#define CASE(X) case X: qDebug() << #X; return;
switch(ret) {
case SEC_E_OK:
break;
CASE(SEC_E_INSUFFICIENT_MEMORY);
CASE(SEC_E_INTERNAL_ERROR);
CASE(SEC_E_INVALID_HANDLE);
CASE(SEC_E_INVALID_TOKEN);
CASE(SEC_E_LOGON_DENIED);
CASE(SEC_E_NO_AUTHENTICATING_AUTHORITY);
CASE(SEC_E_NO_CREDENTIALS);
CASE(SEC_E_TARGET_UNKNOWN);
CASE(SEC_E_UNSUPPORTED_FUNCTION);
CASE(SEC_E_WRONG_PRINCIPAL);
default:
qDebug() << "WAT" << ret;
return;
}
auto pBuffer = outputBufferDesc.pBuffers[0];
QByteArray array((const char *)pBuffer.pvBuffer,pBuffer.cbBuffer);
QNetworkRequest request2 = request;
request2.setRawHeader("Authorization","Negotiate " + array.toBase64());
auto reply2 = manager.get(request2);
reply2->connect(reply2,&QNetworkReply::finished,
[&manager,request,reply2]() {
qDebug() << reply2->rawHeaderList();
qDebug() << reply2->rawHeader("set-cookie");
qDebug() << reply2->readAll();
reply2->deleteLater();
auto reply3 = manager.get(request);
reply3->connect(reply3,&QNetworkReply::finished,
[reply3]() {
qDebug() << reply3->readAll();
qDebug() << reply3->rawHeaderList();
qDebug() << reply3->rawHeader("set-cookie");
reply3->deleteLater();
});
});
}
});
return a.exec();
}
答案 1 :(得分:1)
我将为您提供一个非常简单的Qt5Network库补丁,其中增加了对Negotiate方法的支持。我不知道为什么Qt开发团队拒绝支持Kerberos。所以,
diff -bur network0/access/qhttpnetworkconnection.cpp network/access/qhttpnetworkconnection.cpp
--- network0/access/qhttpnetworkconnection.cpp 2019-08-31 13:29:31.000000000 +0500
+++ network/access/qhttpnetworkconnection.cpp 2019-11-26 16:24:00.832160300 +0500
@@ -52,6 +52,7 @@
#include <qbuffer.h>
#include <qpair.h>
#include <qdebug.h>
+#include <qurl.h>
#ifndef QT_NO_SSL
# include <private/qsslsocket_p.h>
@@ -587,9 +588,14 @@
int i = indexOf(socket);
+ QAuthenticatorPrivate *priv = QAuthenticatorPrivate::getPrivate(channels[i].authenticator);
+ priv->host = QUrl(request.uri(true)).host();
+
// Send "Authorization" header, but not if it's NTLM and the socket is already authenticated.
if (channels[i].authMethod != QAuthenticatorPrivate::None) {
- if ((channels[i].authMethod != QAuthenticatorPrivate::Ntlm && request.headerField("Authorization").isEmpty()) || channels[i].lastStatus == 401) {
+ if ((channels[i].authMethod != QAuthenticatorPrivate::Ntlm
+ && channels[i].authMethod != QAuthenticatorPrivate::Negotiate
+ && request.headerField("Authorization").isEmpty()) || channels[i].lastStatus == 401) {
QAuthenticatorPrivate *priv = QAuthenticatorPrivate::getPrivate(channels[i].authenticator);
if (priv && priv->method != QAuthenticatorPrivate::None) {
QByteArray response = priv->calculateResponse(request.methodName(), request.uri(false));
@@ -601,7 +607,9 @@
// Send "Proxy-Authorization" header, but not if it's NTLM and the socket is already authenticated.
if (channels[i].proxyAuthMethod != QAuthenticatorPrivate::None) {
- if (!(channels[i].proxyAuthMethod == QAuthenticatorPrivate::Ntlm && channels[i].lastStatus != 407)) {
+ if (!((channels[i].proxyAuthMethod == QAuthenticatorPrivate::Ntlm
+ || channels[i].proxyAuthMethod == QAuthenticatorPrivate::Negotiate)
+ && channels[i].lastStatus != 407)) {
QAuthenticatorPrivate *priv = QAuthenticatorPrivate::getPrivate(channels[i].proxyAuthenticator);
if (priv && priv->method != QAuthenticatorPrivate::None) {
QByteArray response = priv->calculateResponse(request.methodName(), request.uri(false));
diff -bur network0/access/qhttpnetworkreply.cpp network/access/qhttpnetworkreply.cpp
--- network0/access/qhttpnetworkreply.cpp 2019-08-31 13:29:31.000000000 +0500
+++ network/access/qhttpnetworkreply.cpp 2019-11-01 15:35:31.207753700 +0500
@@ -421,7 +421,6 @@
for (int i = 0; i<challenges.size(); i++) {
QByteArray line = challenges.at(i);
// todo use qstrincmp
- if (!line.toLower().startsWith("negotiate"))
challenge = line;
}
return !challenge.isEmpty();
@@ -444,6 +443,9 @@
} else if (method < QAuthenticatorPrivate::DigestMd5
&& line.startsWith("digest")) {
method = QAuthenticatorPrivate::DigestMd5;
+ } else if (method < QAuthenticatorPrivate::Negotiate
+ && line.startsWith("negotiate")) {
+ method = QAuthenticatorPrivate::Negotiate;
}
}
return method;
diff -bur network0/kernel/qauthenticator.cpp network/kernel/qauthenticator.cpp
--- network0/kernel/qauthenticator.cpp 2019-08-31 13:29:31.000000000 +0500
+++ network/kernel/qauthenticator.cpp 2019-11-01 15:38:43.108057000 +0500
@@ -377,6 +377,7 @@
switch (method) {
case QAuthenticatorPrivate::Ntlm:
+ case QAuthenticatorPrivate::Negotiate:
if ((separatorPosn = user.indexOf(QLatin1String("\\"))) != -1) {
//domain name is present
realm.clear();
@@ -424,6 +425,9 @@
} else if (method < DigestMd5 && str.startsWith("digest")) {
method = DigestMd5;
headerVal = current.second.mid(7);
+ } else if (method < Negotiate && str.startsWith("negotiate")) {
+ method = Negotiate;
+ headerVal = current.second.mid(10);
}
}
@@ -439,6 +443,7 @@
phase = Done;
break;
case Ntlm:
+ case Negotiate:
// work is done in calculateResponse()
break;
case DigestMd5: {
@@ -477,7 +482,8 @@
phase = Done;
break;
case QAuthenticatorPrivate::Ntlm:
- methodString = "NTLM ";
+ case QAuthenticatorPrivate::Negotiate:
+ methodString = (method == Ntlm) ? "NTLM " : "Negotiate ";
if (challenge.isEmpty()) {
#if defined(Q_OS_WIN) && !defined(Q_OS_WINRT)
QByteArray phase1Token;
@@ -1457,6 +1463,17 @@
{
QByteArray result;
+ QString pkg;
+ QString host;
+ if(ctx->method == ctx->Ntlm) {
+ pkg = QString::fromLatin1("NTLM");
+ host.clear();
+ }
+ else if(ctx->method == ctx->Negotiate) {
+ pkg = QString::fromLatin1("Negotiate");
+ host = QString::fromLatin1("host/%1").arg( ctx->host );
+ }
+
if (!q_NTLM_SSPI_library_load())
return result;
@@ -1467,7 +1484,7 @@
memset(&ctx->ntlmWindowsHandles->credHandle, 0, sizeof(CredHandle));
TimeStamp tsDummy;
SECURITY_STATUS secStatus = pSecurityFunctionTable->AcquireCredentialsHandle(
- NULL, (SEC_WCHAR*)L"NTLM", SECPKG_CRED_OUTBOUND, NULL, NULL,
+ NULL, const_cast<SEC_WCHAR*>( (WCHAR*)pkg.utf16() ), SECPKG_CRED_OUTBOUND, NULL, NULL,
NULL, NULL, &ctx->ntlmWindowsHandles->credHandle, &tsDummy);
if (secStatus != SEC_E_OK) {
delete ctx->ntlmWindowsHandles;
@@ -1489,7 +1506,7 @@
ULONG attrs;
secStatus = pSecurityFunctionTable->InitializeSecurityContext(&ctx->ntlmWindowsHandles->credHandle, NULL,
- const_cast<SEC_WCHAR*>(L"") /* host */,
+ const_cast<SEC_WCHAR*>( (WCHAR*)host.utf16() ) /* host */,
ISC_REQ_ALLOCATE_MEMORY,
0, SECURITY_NETWORK_DREP,
NULL, 0,
diff -bur network0/kernel/qauthenticator_p.h network/kernel/qauthenticator_p.h
--- network0/kernel/qauthenticator_p.h 2019-08-31 13:29:31.000000000 +0500
+++ network/kernel/qauthenticator_p.h 2019-11-01 15:44:20.445370000 +0500
@@ -68,7 +68,7 @@
class Q_AUTOTEST_EXPORT QAuthenticatorPrivate
{
public:
- enum Method { None, Basic, Ntlm, DigestMd5 };
+ enum Method { None, Basic, Ntlm, DigestMd5, Negotiate };
QAuthenticatorPrivate();
~QAuthenticatorPrivate();
@@ -79,6 +79,7 @@
Method method;
QString realm;
QByteArray challenge;
+ QString host;
#ifdef Q_OS_WIN
QNtlmWindowsHandles *ntlmWindowsHandles;
#endif
这是qt5.13.1版本的补丁。如果有人对其他版本的qt5补丁或已经编译的库感兴趣,可以在anselm.ru上找到它们。