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

时间:2014-08-21 21:41:01

标签: java active-directory ldap sasl gssapi

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

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

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

当我针对(&(objectClass=user)(userPrincipalName=sample_user@EXAMPLE.COM))的根DN执行搜索dc=example,dc=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,确定在任何域控制器上没有任何 servicePrincipalName 属性,其值为ldap/example.com。我尝试手动将ldap/example.com的SPN添加到SAVANT-DC1域控制器的计算机帐户。这暂时有效,但Active Directory似乎会在几分钟后自动清除SPN条目。

似乎解决方案是做一个

  1. 获取Active Directory以返回包含域控制器名称而不是域名的连续项。我们知道我们能够以ldap/dc1.example.com
  2. 的形式获取SPN的服务票证
  3. 以某种方式将事件的java端的映射延续为ldap://example.com重定向到ldap://dc1.example.com
  4. 我无法弄清楚如何做(1)。

    我尝试使用JNDI Manual Referral Handling Example作为指南来做(2)。我将 java.naming.referral 属性切换为throw并编写了一个自定义引用处理程序,该处理程序手动覆盖引用中的 java.naming.provider.url 属性上下文。但是LdapReferralException.getReferralContext()似乎忽略了 java.naming.provider.url 环境属性。查看OpenJDK代码到LdapReferralContext.java似乎证实了这一点(第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))]]]
    

1 个答案:

答案 0 :(得分:2)

您不能也不应该使用规范域名注册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和微软关于该主题的文档进行了一些研究。我之前的一些陈述需要恢复,一些更新。以下是我Tomcat SPNEGO/AD Authenticator中记录的解释:

推介处理

使用默认LDAP端口(非GC)或多林环境时,很有可能在搜索或查找期间接收引荐。 JNDI采用多种方法来处理具有java.naming.referral属性及其值的引用:ignore,throw和follow。您可以完全忽略引荐,但是当PartialResultException被迭代时,Active Directory仍会发出NamingEnumeration信号。这个领域将抓住这个并继续处理枚举。如果将DirContextSource设置为throw,则此域将捕获ReferralException,但由于多种原因而无法手动关注引用,并将继续执行该过程。以下引用自动是对应用程序的完全不透明的操作,ReferralException在内部处理,并且引用上下文被查询和关闭。不幸的是,Oracle的LDAP实现无法正确处理,只有Oracle可以解决这个缺点。 有什么缺点,如何解决? Microsoft采用非常复杂的方法不依赖主机名,因为服务器可以随时配置和停用。相反,它们在运行时严重依赖DNS域名和DNS SRV记录。即,引用URL不包含主机名,只包含DNS域名。虽然您可以使用此名称连接到服务,但您无法使用Kerberos轻松对其进行身份验证,因为无法将同一SPN ldap/<dnsDomainName>@<REALM>(例如ldap/example.com@EXAMPLE.COM)绑定到多个帐户。如果您尝试进行身份验证,您将收到“在kerberos数据库中找不到服务器(7)”错误。因此,必须执行DNS SRV查询(_ldap._tcp.<dnsDomainName>)以测试此名称是由一台或多台计算机提供的主机名还是DNS域名。如果它是一个DNS域名,您必须从查询响应中选择一个任意目标主机,构建一个特殊的SPN ldap/<targetHost>/<dnsDomainName>@<REALM>或常规的ldap/<targetHost>@<REALM>,获取服务票证并连接到目标主机。如果它是常规主机名,这不是Active Directory的常见情况,那么Oracle的内部实现将表现正常。 无法使用以下实现,因为无法告诉内部类执行此DNS SRV查询并将SPN的相应服务器名称传递给SaslClient。它被认为是失败的。请注意,主机名canocalization在SaslClient中可能听起来合理,但由于两个原因,这被认为是失败的。首先,SaslClient将接收任意IP地址,而不知道LDAP客户端套接字是否将使用相同的IP地址。您将获得为其他主机颁发的服务票证,您的身份验证将失败。其次,大多数Kerberos实现都依赖于反向DNS记录,但Microsoft的SSPI Kerberos提供程序并不关心反向DNS,它默认情况下不会规范化主机名,并且无法保证反向DNS设置正确。使用throw不会使它更好,因为ReferralException.getReferralInfo()返回的引用URL无法使用DNS中的计算值更改。 ReferralException.getReferralContext()将无条件地重用该值。实现此目的的唯一方法(理论上)是手动构造带有新URL的InitialDirContext并适当地使用它。但是,这种方法尚未经过评估,目前尚未实施。 (在调试器中手动更改URL使其实际工作)

如何解决此问题?尽可能多地使用全局编录(端口3268)。如果这样做无济于事,并且您预先知道目标林,则可以设置CombinedRealm,使用ignore为每个林配置一个嵌套域,并让主体遍历所有林,直到它到达目标林。然后,您将在Active Directory中正确查找客户端。