繁体   English   中英

在 Java 中使用 GSSAPI 身份验证时,来自 Active Directory 的搜索结果中的 LDAP Continuation Reference 错误

[英]LDAP Continuation Reference error in search results from Active Directory when using GSSAPI authentication in Java

更新:根据下面@Michael-O 的评论,如果 LDAP JNDI 提供程序或 SASL 实现通过执行转发然后反向 DNS 查找来规范化主机名,这似乎是处理此问题的正确方法。 KRN 服务票据请求。 我将尝试联系 Open JDK 安全列表,看看是否有任何答案从那里返回。

我正在尝试使用通过 GSSAPI 使用 Kerberos LoginContext 中的主题进行身份验证的会话针对 Active Directory 服务器在根 DN 上执行递归 LDAP 搜索。

我能够使用 URL ldap://dc1.example.com成功绑定到服务器。 InitialDirContextjava.naming.referral设置为follow

当我对dc=example,dc=com的根 DN 执行搜索(&(objectClass=user)(userPrincipalName=sample_user@EXAMPLE.COM)) ,我得到一个SearchResult

CN=Sample User,OU=ExampleUsers,DC=example,DC=com

和几个继续参考:

ldap://example.com/CN=Configuration,DC=example,DC=com
ldap://ForestDnsZones.example.com/DC=ForestDnsZones,DC=example,DC=com
ldap://DomainDnsZones.example.com/DC=DomainDnsZones,DC=example,DC=com

我可以很好地迭代SearchResult ,但是一旦遇到延续,我就会得到PartialResultsException 我检查了 DNS 并且上述所有主机名都正确解析。 我得到的异常如下所示:

javax.naming.PartialResultException 
  [Root exception is javax.naming.AuthenticationException: GSSAPI 
    [Root exception is javax.security.sasl.SaslException: GSS initiate failed 
      [Caused by GSSException: No valid credentials provided 
        (Mechanism level: Server not found in Kerberos database (7))]]].

查看 Kerberos 跟踪,此错误是有道理的。 当尝试遵循延续时,LDAP 库尝试绑定到ldap://example.com 由于我们使用 GSSAPI 进行身份验证,这会触发ldap/example.com的服务票证请求。 我在日志中看到的响应是:

>>>KRBError:
     sTime is Thu Aug 21 14:27:20 EDT 2014 0000000000000
     suSec is 414575
     error code is 7
     error Message is Server not found in Kerberos database
     realm is EXAMPLE.COM
     sname is ldap/example.com
     msgType is 30

我检查了 Active Directory,果然在任何域控制器上的任何地方都没有任何值为ldap/example.com servicePrincipalName属性。 我尝试将ldap/example.com的 SPN 手动添加到 SAVANT-DC1 域控制器的计算机帐户。 这暂时有效,但 Active Directory 似乎会在几分钟后自动清除 SPN 条目。

似乎解决方案是执行以下操作之一

  1. 获取 Active Directory 以返回包含域控制器名称而不是域的延续。 我们知道我们能够以ldap/dc1.example.com的形式获取 SPN 的服务票证。
  2. 以某种方式映射 java 端的延续以将ldap://example.com重定向到ldap://dc1.example.com

我无法弄清楚该怎么做(1)。

我尝试使用JNDI 手册引用处理示例作为指南来做 (2)。 我将java.naming.referral属性切换为throw并编写了一个自定义引用处理程序,该处理程序手动覆盖引用上下文中的java.naming.provider.url属性。 然而LdapReferralException.getReferralContext()似乎忽略了java.naming.provider.url环境属性。 查看LdapReferralContext.java的 OpenJDK 代码似乎证实了这一点(第 105 行)。

这就是我所在的位置:我无法拦截和操作 Java 端的引用,因为它们被 JNDI API 视为黑盒。 我无法在 AD 端手动创建 LDAP SPN,因为它不会在目录中保持持久性。 还有什么我想念的吗?


这是我正在运行的代码

import java.io.File;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.ReferralException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;

public class LdapContinuationDemoAction implements PrivilegedExceptionAction<Object> {
  private final String ldapUrl;
  private final String ldapDn;
  private final String username;

  public static void main(String[] argv) {
    try {
      String username = "example_user@EXAMPLE.COM";
      String password = "Password1";
      String ldapUrl  = "ldap://dc1.example.com";
      String searchDn = "dc=example,dc=com";
      String pwd      = System.getProperty("user.dir");
      String krb5Conf = new File(pwd, "krb5.conf").getAbsolutePath();

      System.setProperty("java.security.krb5.conf", krb5Conf);
      System.setProperty("sun.security.krb5.debug", "true");

      // Login to the domain via Kerberos
      LoginContext loginCtx = new LoginContext("doesn't matter", null,
        getUsernamePasswordHandler(username, password),
        getKrb5Configuration());

      System.out.println("********************************");
      System.out.println("      KRB5 Login");
      System.out.println("********************************");
      loginCtx.login();

      // Execute the LDAP search as the user logged in above
      LdapContinuationDemoAction action = new LdapContinuationDemoAction(ldapUrl,
        searchDn, username);

      Subject.doAs(loginCtx.getSubject(), action);
    } catch( Exception e) {
      System.out.println();
      System.out.println("*** ERROR: " + e);
    }
  }

  private LdapContinuationDemoAction(String ldapUrl, String ldapDn,
    String username) {
    this.ldapUrl  = ldapUrl;
    this.ldapDn   = ldapDn;
    this.username = username;
  }

  // Perform a recursive LDAP search for a user principal and print the results
  @Override
  public Object run() throws Exception {
    System.out.println("********************************");
    System.out.println("      LDAP Login");
    System.out.println("********************************");

    //Setup the directory context environment
    Properties dirCtxProps = new Properties();
    dirCtxProps.put(Context.INITIAL_CONTEXT_FACTORY,      "com.sun.jndi.ldap.LdapCtxFactory");
    dirCtxProps.put(Context.PROVIDER_URL,                 this.ldapUrl);
    dirCtxProps.put(Context.SECURITY_AUTHENTICATION,      "GSSAPI");
    dirCtxProps.put("java.naming.ldap.attributes.binary", "objectSID");
    dirCtxProps.put(Context.REFERRAL,                     "follow");

    DirContext dirCtx = new InitialDirContext(dirCtxProps);

    // enable recursive searching
    SearchControls ctrls = new SearchControls();
    ctrls.setSearchScope(SearchControls.SUBTREE_SCOPE);

    // do the search
    NamingEnumeration<SearchResult> results = dirCtx.search(this.ldapDn,
      "(&(objectClass=user)(userPrincipalName={0}))",
      new Object[] { this.username }, ctrls);

    System.out.println("********************************");
    System.out.println("      LDAP User Info");
    System.out.println("********************************");
    int resultNum = 0;

    while (results.hasMore()) {
      resultNum++;

      Attributes userAttr = results.next().getAttributes();

      System.out.println("ldap result " + resultNum + ": User DN: "
        + userAttr.get("distinguishedName").get());
      System.out.println();
    }


    return null;
  }

  // JAAS callback handler for username and password Kerberos authn
  private static CallbackHandler getUsernamePasswordHandler(
    final String username, final String password) {

    final CallbackHandler handler = new CallbackHandler() {
      @Override
      public void handle(final Callback[] callback) {
        for (int i = 0; i < callback.length; i++) {
          if (callback[i] instanceof NameCallback) {
            final NameCallback nameCallback = (NameCallback) callback[i];
            nameCallback.setName(username);
          } else if (callback[i] instanceof PasswordCallback) {
            final PasswordCallback passCallback = (PasswordCallback) callback[i];
            passCallback.setPassword(password.toCharArray());
          } else {
            System.err.println("Unsupported Callback: "
              + callback[i].getClass().getName());
          }
        }
      }
    };

    return handler;
  }

  // dynamically build a Kerberos JAAS configuration so we don't need a login.conf
  private static Configuration getKrb5Configuration() {
    return new Configuration() {

      @Override
      public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
        Map<String, String> options = new HashMap<String, String>();
        options.put("client", "true");
        return new AppConfigurationEntry[] {
          new AppConfigurationEntry(
            "com.sun.security.auth.module.Krb5LoginModule",
            LoginModuleControlFlag.REQUIRED, options)
        };
      }
    };
  }

}

这是我的 krb5.conf:

[libdefaults]
  default_realm = EXAMPLE.COM

[realms]
  EXAMPLE.COM = {
    kdc = dc1.example.com
    default_domain = example.com
  }

[domain_realm]
  .example.com = EXAMPLE.COM
  example.com = EXAMPLE.COM

这是上面代码的输出

********************************
      KRB5 Login
********************************
Config name: C:\src\scratch\krb5\krb5.conf
>>> KdcAccessibility: reset
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 16 23 1 3.
>>> KrbAsReq creating message
>>> KrbKdcReq send: kdc=dc1.example.com UDP:88, timeout=30000, number of retries =3, #bytes=158
>>> KDCCommunication: kdc=dc1.example.com UDP:88, timeout=30000,Attempt =1, #bytes=158
>>> KrbKdcReq send: #bytes read=227
>>>Pre-Authentication Data:
     PA-DATA type = 19
     PA-ETYPE-INFO2 etype = 18, salt = EXAMPLE.COMexample_user, s2kparams = null
     PA-ETYPE-INFO2 etype = 23, salt = null, s2kparams = null
     PA-ETYPE-INFO2 etype = 3, salt = EXAMPLE.COMexample_user, s2kparams = null

>>>Pre-Authentication Data:
     PA-DATA type = 2
     PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
     PA-DATA type = 16

>>>Pre-Authentication Data:
     PA-DATA type = 15

>>> KdcAccessibility: remove dc1.example.com
>>> KDCRep: init() encoding tag is 126 req type is 11
>>>KRBError:
     sTime is Thu Aug 21 16:35:42 EDT 2014 0000000000000
     suSec is 659371
     error code is 25
     error Message is Additional pre-authentication required
     realm is EXAMPLE.COM
     sname is krbtgt/EXAMPLE.COM
     eData provided.
     msgType is 30
>>>Pre-Authentication Data:
     PA-DATA type = 19
     PA-ETYPE-INFO2 etype = 18, salt = EXAMPLE.COMexample_user, s2kparams = null
     PA-ETYPE-INFO2 etype = 23, salt = null, s2kparams = null
     PA-ETYPE-INFO2 etype = 3, salt = EXAMPLE.COMexample_user, s2kparams = null

>>>Pre-Authentication Data:
     PA-DATA type = 2
     PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
     PA-DATA type = 16

>>>Pre-Authentication Data:
     PA-DATA type = 15

KrbAsReqBuilder: PREAUTH FAILED/REQ, re-send AS-REQ
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 16 23 1 3.
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 16 23 1 3.
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> KrbAsReq creating message
>>> KrbKdcReq send: kdc=dc1.example.com UDP:88, timeout=30000, number of retries =3, #bytes=240
>>> KDCCommunication: kdc=dc1.example.com UDP:88, timeout=30000,Attempt =1, #bytes=240
>>> KrbKdcReq send: #bytes read=1425
>>> KdcAccessibility: remove dc1.example.com
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> KrbAsRep cons in KrbAsReq.getReply example_user
********************************
      LDAP Login
********************************
Found ticket for example_user@EXAMPLE.COM to go to krbtgt/EXAMPLE.COM@EXAMPLE.COM expiring on Fri Aug 22 02:35:42 EDT 2014
Entered Krb5Context.initSecContext with state=STATE_NEW
Found ticket for example_user@EXAMPLE.COM to go to krbtgt/EXAMPLE.COM@EXAMPLE.COM expiring on Fri Aug 22 02:35:42 EDT 2014
Service ticket not found in the subject
>>> Credentials acquireServiceCreds: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 16 23 1 3.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
>>> KrbKdcReq send: kdc=dc1.example.com UDP:88, timeout=30000, number of retries =3, #bytes=1392
>>> KDCCommunication: kdc=dc1.example.com UDP:88, timeout=30000,Attempt =1, #bytes=1392
>>> KrbKdcReq send: #bytes read=1398
>>> KdcAccessibility: remove dc1.example.com
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
>>> KrbApReq: APOptions are 00000000 00000000 00000000 00000000
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
Krb5Context setting mySeqNumber to: 774790609
Krb5Context setting peerSeqNumber to: 0
Created InitSecContextToken:
0000: 01 00 6E 00 00 00 00 00   00 00 A0 03 02 01 05 A1  ..n..)0..%......
0010: 03 02 01 0E A2 00 00 00   00 00 00 00 00 A3 82 04  ................
0020: 00 00 00 00 00 00 00 00   2D A0 03 02 01 05 A1 0E  5a..10..-.......
0030: 1B 0C 55 54 42 53 41 56   2E 4C 4F 43 41 4C A2 2A  ..EXAMPLE.COM.*
0040: 30 28 A0 03 02 01 00 A1   21 30 1F 1B 04 6C 64 61  0(......!0...lda
0050: 70 1B 17 73 61 76 61 6E   74 2D 64 63 31 2E 75 74  p..dc1.ut
0060: 62 73 61 76 2E 6C 6F 63   61 6C A3 82 03 E8 30 82  bsav.local....0.
0070: 03 E4 A0 03 02 01 12 A1   03 02 01 08 A2 82 03 D6  ................
---8<--- Snipping a bunch of binary

Krb5Context.unwrap: token=[05 04 01 ff 00 0c 00 0c 00 00 00 00 2e 2e 5d d1 f5 d2 e8 21 c1 23 92 20 61 f4 77 a8 07 a0 00 00 ]
Krb5Context.unwrap: data=[07 a0 00 00 ]
Krb5Context.wrap: data=[01 01 00 00 ]
Krb5Context.wrap: token=[05 04 00 ff 00 0c 00 00 00 00 00 00 2e 2e 5d d1 00 00 00 00 00 00 00 00 fa b6 79 67 ce db 58 d2 ]
********************************
      LDAP User Info
********************************
ldap result 1: User DN: CN=Sample User,OU=ExampleUsers,DC=example,DC=com

Found ticket for example_user@EXAMPLE.COM to go to krbtgt/EXAMPLE.COM@EXAMPLE.COM expiring on Fri Aug 22 02:35:42 EDT 2014
Entered Krb5Context.initSecContext with state=STATE_NEW
Found ticket for example_user@EXAMPLE.COM to go to krbtgt/EXAMPLE.COM@EXAMPLE.COM expiring on Fri Aug 22 02:35:42 EDT 2014
Found ticket for example_user@EXAMPLE.COM to go to ldap/dc1.example.com@EXAMPLE.COM expiring on Fri Aug 22 02:35:42 EDT 2014
Service ticket not found in the subject
>>> Credentials acquireServiceCreds: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 16 23 1 3.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
>>> KrbKdcReq send: kdc=dc1.example.com UDP:88, timeout=30000, number of retries =3, #bytes=1381
>>> KDCCommunication: kdc=dc1.example.com UDP:88, timeout=30000,Attempt =1, #bytes=1381
>>> KrbKdcReq send: #bytes read=94
>>> KdcAccessibility: remove dc1.example.com
>>> KDCRep: init() encoding tag is 126 req type is 13
>>>KRBError:
     sTime is Thu Aug 21 16:35:46 EDT 2014 0000000000000
     suSec is 918178
     error code is 7
     error Message is Server not found in Kerberos database
     realm is EXAMPLE.COM
     sname is ldap/example.com
     msgType is 30
KrbException: Server not found in Kerberos database (7)
    at sun.security.krb5.KrbTgsRep.<init>(KrbTgsRep.java:70)
    at sun.security.krb5.KrbTgsReq.getReply(KrbTgsReq.java:192)
    at sun.security.krb5.KrbTgsReq.sendAndGetCreds(KrbTgsReq.java:203)
    at sun.security.krb5.internal.CredentialsUtil.serviceCreds(CredentialsUtil.java:311)
    at sun.security.krb5.internal.CredentialsUtil.acquireServiceCreds(CredentialsUtil.java:115)
    at sun.security.krb5.Credentials.acquireServiceCreds(Credentials.java:442)
    at sun.security.jgss.krb5.Krb5Context.initSecContext(Krb5Context.java:641)
    at sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:248)
    at sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:179)
    at com.sun.security.sasl.gsskerb.GssKrb5Client.evaluateChallenge(GssKrb5Client.java:193)
    at com.sun.jndi.ldap.sasl.LdapSasl.saslBind(LdapSasl.java:123)
    at com.sun.jndi.ldap.LdapClient.authenticate(LdapClient.java:232)
    at com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2740)
    at com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:316)
    at com.sun.jndi.ldap.LdapCtxFactory.getUsingURL(LdapCtxFactory.java:193)
    at com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance(LdapCtxFactory.java:152)
    at com.sun.jndi.url.ldap.ldapURLContextFactory.getObjectInstance(ldapURLContextFactory.java:52)
    at javax.naming.spi.NamingManager.getURLObject(NamingManager.java:601)
    at javax.naming.spi.NamingManager.processURL(NamingManager.java:381)
    at javax.naming.spi.NamingManager.processURLAddrs(NamingManager.java:361)
    at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:333)
    at com.sun.jndi.ldap.LdapReferralContext.<init>(LdapReferralContext.java:111)
    at com.sun.jndi.ldap.LdapReferralException.getReferralContext(LdapReferralException.java:150)
    at com.sun.jndi.ldap.LdapNamingEnumeration.hasMoreReferrals(LdapNamingEnumeration.java:357)
    at com.sun.jndi.ldap.LdapNamingEnumeration.hasMoreImpl(LdapNamingEnumeration.java:226)
    at com.sun.jndi.ldap.LdapNamingEnumeration.hasMore(LdapNamingEnumeration.java:189)
    at LdapContinuationDemoAction.run(LdapContinuationDemoAction.java:123)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAs(Subject.java:415)
    at LdapContinuationDemoAction.main(LdapContinuationDemoAction.java:52)
Caused by: KrbException: Identifier doesn't match expected value (906)
    at sun.security.krb5.internal.KDCRep.init(KDCRep.java:143)
    at sun.security.krb5.internal.TGSRep.init(TGSRep.java:66)
    at sun.security.krb5.internal.TGSRep.<init>(TGSRep.java:61)
    at sun.security.krb5.KrbTgsRep.<init>(KrbTgsRep.java:55)
    ... 29 more

*** ERROR: java.security.PrivilegedActionException: javax.naming.PartialResultException [Root exception is javax.naming.AuthenticationException: GSSAPI [Root exception is javax.security.sasl.SaslException: GSS initiate failed [Caused by GSSException: No valid credentials provided (Mechanism level: Server not found in Kerberos database (7))]]]

您不能也不应使用规范领域名称注册 SPN。 在这种情况下,SPN 必须是特定于机器的。 如果您真的想使用ldap://example.com ,请确保在构建 SPN 之前执行反向 DNS。 默认情况下,MIT Kerberos、Heimdal 和 JGSS 将执行反向 DNS 查找,但 SSPI 不会,因此这是不现实的。

更好的解决方案不是提供主机名,而是使用 DNS SRV 来定位 DC,然后执行绑定。 因此,将您的URL更改为ldap:///DC=example,DC=com

编辑 (2016-03-14):1,5 年多之后,我自己在工作中偶然发现了这一点,并使用 Windows 工具、Wireshark 和 Microsoft 的有关该主题的文档进行了一些研究。 我之前的一些陈述需要恢复,一些更新。 这是我的Tomcat SPNEGO/AD Authenticator 中也记录的解释:

编辑 (2021-06-06):对于那些仍然受此困扰的人,使用我的Active Directory DNS 定位器即可完成。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM