在Qt中实现SPNEGO

3

我需要在Qt客户端中实现SPNEGO。服务器响应401/未授权并发送WWW-Authenticate: Negotiate头。

2个回答

6
首先,通过这里的RFC了解协议:https://www.rfc-editor.org/rfc/rfc4559。另一个很好的参考资料是:https://github.com/requests/requests-kerberos
请注意,虽然有跨平台的实现,但 GSSAPI 在 Windows 上并不存在,相反你需要使用 SSPI。本答案将展示如何在 Windows 上实现它。你可以将这些函数映射到其他平台的 GSSAPI 上。你永远不会为真正的代码编写像这样的代码,但我创建了这个代码供那些需要实现 SPNEGO 且不需过多抽象时使用。
此示例使用 Brandon-Godwin 创建的出色的 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
我将为您提供一个极其简单的补丁,用于为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找到它们。

这很棒,但缺点是您必须重新构建QtNetwork,这可能对您来说是一个问题或不是问题。有没有办法在不打补丁的情况下完成它? - Sohail
Qt的一个巨大缺陷是存在太多私有类。因此,您无法直接在程序中重新定义许多类。唯一的出路是重新构建qt库本身。不幸的是,这非常糟糕,Qt正在走向错误的方向。 - AnselmRu
1
今天发现需要添加两个(!)主体: setspn -A HTTP/server.domain.com username & setspn -A host/server.domain.com username - AnselmRu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接