在Qt中实现SPNEGO

时间:2016-06-22 12:55:05

标签: qt spnego

我需要使用Qt客户端实现SPNEGO。服务器以401 / Unauthorized响应并发送WWW-Authenticate:Negotiate标头。

2 个答案:

答案 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上找到它们。