java - 使用 SSPI : no errors, 进行 Kerberos 模拟但不起作用

标签 java kerberos jna impersonation sspi

我需要在 Java 应用程序服务器中模拟用户,并使用该用户的权限对 IIS 中的 ASP 应用程序执行 http 请求。为此,我正在尝试调整 Apache HttpClient 的 WindowsNegotiateScheme 类。这使用 JNA 直接访问 Windows SSPI 身份验证功能,因此问题可能不是 Java 特有的。由于我需要“协议(protocol)转换”,因此我没有可以模拟的用户密码,只有名称。但使用 S4U 功能,这应该是可能的。

所有 SSPI 函数调用要么返回 0(成功),要么返回 590610 (SEC_I_CONTINUE_NEEDED),永远不会返回错误代码,并且代码会运行,但 http 请求会随着用户运行应用程序服务器而到达 ASP 端,无论我是否在我的工作站上以我的用户 ID 本地运行它,或者在应用程序服务器(恰好是也运行 IIS/ASP 应用程序的计算机)中运行它,该服务器作为本地系统运行。

相关部分代码如下:

public Header authenticate(Credentials credentials, HttpRequest request, HttpContext context) throws AuthenticationException {
    final String response;
    if (clientCred == null) { // first time
        boolean impersonate = ...; // logic not relevant here

        try {
            final String username = impersonate ? credentials.getUserPrincipal().getName() : CurrentWindowsCredentials.getCurrentUsername();
            final Sspi.TimeStamp lifetime = new Sspi.TimeStamp();
            this.clientCred = new Sspi.CredHandle();
            int credUse = impersonate ? (Sspi.SECPKG_CRED_OUTBOUND | Sspi.SECPKG_CRED_INBOUND) : Sspi.SECPKG_CRED_OUTBOUND;

            int rc = Secur32.INSTANCE.AcquireCredentialsHandle(username,
                    scheme, credUse, null, null, null, null,
                    clientCred, lifetime);
            if (WinError.SEC_E_OK != rc) {
                throw new Win32Exception(rc);
            }

            if(impersonate) {
                impersonationContext = getLocalContext(clientCred);
                rc = Secur32.INSTANCE.ImpersonateSecurityContext(impersonationContext);
            } else {
                impersonationContext = null;
            }

            String targetSpn = getServicePrincipalName(context);
            response = getToken(null, null, targetSpn);
        } catch (RuntimeException ex) {
            ...
        }
    } else {
        ... // second round
    }
    ... // build header form response and return it
}

方法getLocalContext()旨在为模拟用户获取服务本身的上下文。它看起来如下:

private static Sspi.CtxtHandle getLocalContext(final Sspi.CredHandle initialCredentials) {
    final IntByReference attr = new IntByReference();

    String localSpn = "HOST/" + Kernel32Util.getComputerName().toLowerCase();

    Sspi.CtxtHandle clientContext = null;
    Sspi.CtxtHandle serverContext = null;

    Sspi.SecBufferDesc clientToServerToken;
    Sspi.SecBufferDesc serverToClientToken = null;
    while(true) {
        // get clientContext and clientToServerToken:
        Sspi.CtxtHandle outClientContext = (clientContext != null) ? clientContext : new Sspi.CtxtHandle();
        clientToServerToken = new Sspi.SecBufferDesc(Sspi.SECBUFFER_TOKEN, Sspi.MAX_TOKEN_SIZE);
        int rc = Secur32.INSTANCE.InitializeSecurityContext(initialCredentials,
                clientContext, localSpn, Sspi.ISC_REQ_DELEGATE | Sspi.ISC_REQ_MUTUAL_AUTH, 0,
                Sspi.SECURITY_NATIVE_DREP, serverToClientToken, 0, outClientContext, clientToServerToken,
                attr, null);
        clientContext = outClientContext; // make them same for next round
        switch (rc) {
            case WinError.SEC_I_CONTINUE_NEEDED:
                break;
            case WinError.SEC_E_OK:
                return serverContext != null ? serverContext : clientContext;
            default:
                freeContexts(clientContext, serverContext);
                throw new Win32Exception(rc);
        }

        // get serverContext and serverToClientToken:
        Sspi.CtxtHandle outServerContext = (serverContext != null) ? serverContext : new Sspi.CtxtHandle();
        serverToClientToken = new Sspi.SecBufferDesc(Sspi.SECBUFFER_TOKEN, Sspi.MAX_TOKEN_SIZE);
        rc = Secur32.INSTANCE.AcceptSecurityContext(initialCredentials,
                serverContext, clientToServerToken, Sspi.ISC_REQ_DELEGATE | Sspi.ISC_REQ_MUTUAL_AUTH,
                Sspi.SECURITY_NATIVE_DREP, outServerContext, serverToClientToken, attr, null);
        serverContext = outServerContext; // make them same for next round
        switch (rc) {
            case WinError.SEC_I_CONTINUE_NEEDED:
                break;
            case WinError.SEC_E_OK:
                return serverContext;
            default:
                freeContexts(clientContext, serverContext);
                throw new Win32Exception(rc);
        }
    }
}

我发现它在返回之前运行了完整的两轮( InitializeSecurityContextAcceptSecurityContextInitializeSecurityContextAcceptSecurityContext )。

作为引用,getToken方法只是从原始WinHttpClient WindowsNegotiateScheme复制而来类:

String getToken(
        final CtxtHandle continueCtx,
        final SecBufferDesc continueToken,
        final String targetName) {
    final IntByReference attr = new IntByReference();
    final SecBufferDesc token = new SecBufferDesc(
            Sspi.SECBUFFER_TOKEN, Sspi.MAX_TOKEN_SIZE);

    sspiContext = new CtxtHandle();
    final int rc = Secur32.INSTANCE.InitializeSecurityContext(clientCred,
            continueCtx, targetName, Sspi.ISC_REQ_DELEGATE | Sspi.ISC_REQ_MUTUAL_AUTH, 0,
            Sspi.SECURITY_NATIVE_DREP, continueToken, 0, sspiContext, token,
            attr, null);
    switch (rc) {
        case WinError.SEC_I_CONTINUE_NEEDED:
            continueNeeded = true;
            break;
        case WinError.SEC_E_OK:
            dispose(); // Don't keep the context
            continueNeeded = false;
            break;
        default:
            dispose();
            throw new Win32Exception(rc);
    }
    return Base64.encodeBase64String(token.getBytes());
}

我在调试器中检查了用于模拟的用户名实际上类似于 DOMAINNAME\johndoe ,我意识到即使我使用不存在的用户也没关系,行为是一样的: AcquireCredentialsHandle返回 0 (= SEC_E_OK)。这告诉我,此方法不会根据域 Controller 检查用户名,而只会检查其参数的语法,但是为什么初始调用 InitializeSecurityContext如果第一个参数是不存在的用户的凭据,不会提示吗?或者该方法以某种方式完全忽略了它的第一个参数,但为什么呢?

假设我已经有一个供用户模拟的上下文 AcquireCredentialsHandle调用并省略对 getLocalContext 的调用和ImpersonateSecurityContext也没有用。

我发现没有太多关于使用纯 SSPI 进行模拟的文档。我发现的大多数示例(例如 this )依赖于 .net 类 WindowsIdentity ,这在纯 SSPI 接口(interface)中是不可用的。还有source code该类的内容看起来并不只是由几个 SSPI 调用组成,而是引用了许多其他 .net 基础结构。

我认为 Windows 操作系统模拟可能无法与 Java 正确协作,但根据 this question ,Windows 模拟是针对每个线程的,并且根据 this question ,Java 使用操作系统线程。

我怎样才能让模拟工作,或者你至少有一些尝试的提示?

最佳答案

您的代码永远不会执行您所描述的操作。您的代码片段仅执行凭证委派(无约束委派),其中转发的 TGT 嵌入到安全上下文中。由于您需要进行协议(protocol)转换(S4U2Self),因此需要调用LsaLogonUserKERB_S4U_LOGON .

阅读this博客文章。在 C 中这将是一个很大的痛苦。从来不明白为什么这对于 GSS-API 来说如此简单,但在 Windows 上却如此痛苦。

祝你好运。

关于java - 使用 SSPI : no errors, 进行 Kerberos 模拟但不起作用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48460008/

相关文章:

java - 从 JPA 嵌入表中的 POST 访问表单数据

java - Apache shiro + kerberos 身份验证

java - 如何从 jna 引用指针检索 java 类

Java native 访问 - GetExtendedTcpTable : Bounds exceeds available space

java - CreateProcessW 失败 (ACCESS_DENIED)

java - 使用 BigDecimal 将比例设置为负数

java - 为什么 Java 8 中的新 java.util.Arrays 方法没有为所有原始类型重载?

java - 如何使用 apache poi 为 3 个单元格设置注释

java - HttpClient 设置 Kerberos 身份验证的凭据

java - GSSAPI缺陷票