DirContext初始化过程分析

本文针对LDAP认证失败问题,从初始化过程入手,详细分析了使用Java JNDI API时出现AuthenticationException异常的原因。通过逐步调试发现,问题根源在于principal设置不正确。
这篇博文的原因

主要是因为犯了一个很愚蠢的错误,在实例化DirContext的时候报了AuthenticationException,错误码为49,我很疑惑,同样的方式通过ldapBrowser可以连接,在JNDI却不能连接了,同样尝试了Java的Apache Directory Studio插件也是不能连接,但是匿名连接却连接得上。基于这个原因便开始了探究初始化过程的分析,不过最后的结论却是——只是我的principal错了而已,我一直以为是我的credentials的问题。为什么同样用cn=Manager通过LDAPBrowser能访问,而通过JNDI或者ADS插件却不能访问呢?理由(大概)是访问的时候完整的用户dn应该都是cn=Manager,dc=maxcrc,dc=com,这个你定义在slap.conf的rootDn,而为什么ldapBrowser能够通过cn=Manager来访问呢?那是因为它有进行了一些拼接,因此我们总以为A是这样所以B应该是这样的想法有时候不一定通用,特别是当它们属于两个不同的项目的时候。虽然这样一来分析源码的意义不存在了,但是还是把分析过程贴出来和大家分享分享,过程也是比较简略,而且针对一种情况分析。

分析过程

该分析过程建立于以下基础:

        evn.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");//导入提供者
        evn.put(Context.PROVIDER_URL, "ldap://localhost/dc=maxcrc,dc=com");//服务器地址
        evn.put(Context.SECURITY_AUTHENTICATION, "simple");//验证方式
        evn.put(Context.SECURITY_PRINCIPAL, "cn=Manager,dc=maxcrc,dc=com");//账户
        evn.put(Context.SECURITY_CREDENTIALS, "chouxiaohai");//证书

首先在InitialDirContext中调用父类InitialContext的构造函数,evn进行一些处理之后调用init方法,init方法如下:

    protected void init(Hashtable<?,?> environment)
        throws NamingException
    {
        myProps = (Hashtable<Object,Object>)
                ResourceManager.getInitialEnvironment(environment);

        if (myProps.get(Context.INITIAL_CONTEXT_FACTORY) != null) {
            // user has specified initial context factory; try to get it
            getDefaultInitCtx();
        }
    }

在getDefaultInitCtx调用NamingManager的getInitialContext,我们应该认为这才是制造DirContext的工厂。在这里进行了如下操作:


                ...

//获取工厂类名
String className = env != null ?
                (String)env.get(Context.INITIAL_CONTEXT_FACTORY) : null;

                ...

//获得工厂类实例
factory = (InitialContextFactory)
                    helper.loadClass(className).newInstance();

                ...

//通过工厂类构建上下文
return factory.getInitialContext(env);

在getInitialContext中主要操作如下:

                ...
//获取ldapURL             
String str = paramHashtable != null ? (String)paramHashtable.get("java.naming.provider.url") : null;

                ...
//切割成string数组
arrayOfString = LdapURL.fromList(str);

                ...
//获取LdapCtx实例
return getLdapCtxInstance(arrayOfString, paramHashtable);

getLdapCtxInstance如下:

{           
    if ((paramObject instanceof String))
        return getUsingURL((String)paramObject, paramHashtable);
    if ((paramObject instanceof String[])) {
        return getUsingURLs((String[])paramObject, paramHashtable);
}

因为我们是数组,所以这里调用的是第二个函数:

                ...

for (int i = 0; i < paramArrayOfString.length; i++) {
    try {
        return getUsingURL(paramArrayOfString[i], paramHashtable);
    } catch ...

说实话我也不明白他为什么这么操作,它被URL切割成数组后实际上也只有一个值,因为它是根据“ ”切割的,而这里即使有多个值它也只处理一个值,讲道理我实在不明白,是为了代码健壮性?
getUsingURL这个方法名我们可以理解成通过URL获取目录服务上下文,现在进入它的方法体:

                ...
localObject = new LdapCtx(str1, str2, i, paramHashtable, localLdapURL.useSsl());
                ...

它主要是调用了这个方法,其中str1是DN,str2是host,i是port,paramHashtable是环境变量,不过已经经过了一些操作,useSsl返回true或false,它表示是否通过ssl验证,我们这里只是简单验证,因此为false.
LdapCtx的构造函数如下定义:

                ...
if ("ssl".equals(this.envprops.get("java.naming.security.protocol"))) {
    this.useSsl = true;
}
                ...

我们可以看到,假如我们通过ssl验证的话,那我们给环境变量加上java.naming.security.protocol这个属性,并设值为ssl。此外在这里进行了设值操作,并且调用initEvn把所有的值设进去,initEvn如下:

private void initEnv() throws NamingException {
/* 2330 */     if (this.envprops == null)
/*      */     {
/* 2332 */       setReferralMode(null, false);
/* 2333 */       return;
/*      */     }
/*      */     
/*      */ 
/* 2337 */     setBatchSize((String)this.envprops.get("java.naming.batchsize"));
/*      */     
/*      */ 
/* 2340 */     setRefSeparator((String)this.envprops.get("java.naming.ldap.ref.separator"));
/*      */     
/*      */ 
/* 2343 */     setDeleteRDN((String)this.envprops.get("java.naming.ldap.deleteRDN"));
/*      */     
/*      */ 
/* 2346 */     setTypesOnly((String)this.envprops.get("java.naming.ldap.typesOnly"));
/*      */     
/*      */ 
/* 2349 */     setDerefAliases((String)this.envprops.get("java.naming.ldap.derefAliases"));
/*      */     
/*      */ 
/* 2352 */     setReferralLimit((String)this.envprops.get("java.naming.ldap.referral.limit"));
/*      */     
/* 2354 */     setBinaryAttributes((String)this.envprops.get("java.naming.ldap.attributes.binary"));
/*      */     
/* 2356 */     this.bindCtls = cloneControls((Control[])this.envprops.get("java.naming.ldap.control.connect"));
/*      */     
/*      */ 
/* 2359 */     setReferralMode((String)this.envprops.get("java.naming.referral"), false);
/*      */     
/*      */ 
/* 2362 */     setConnectTimeout((String)this.envprops.get("com.sun.jndi.ldap.connect.timeout"));
/*      */     
/*      */ 
/* 2365 */     setReadTimeout((String)this.envprops.get("com.sun.jndi.ldap.read.timeout"));
/*      */     
/*      */ 
/*      */ 
/* 2369 */     setWaitForReply((String)this.envprops.get("com.sun.jndi.ldap.search.waitForReply"));
/*      */     
/*      */ 
/* 2372 */     setReplyQueueSize((String)this.envprops.get("com.sun.jndi.ldap.search.replyQueueSize"));
/*      */   }

之后调用connect方法,我们可以认定,认证操作就在这里面了。让我们进去看看:


                ...

str1 = (String)this.envprops.get("java.naming.security.principal");
localObject1 = this.envprops.get("java.naming.security.credentials");
str5 = (String)this.envprops.get("java.naming.ldap.version");

                ...

//实例化该对象
this.clnt = LdapClient.getInstance(bool1, this.hostname, this.port_number, str3, this.connectTimeout, this.readTimeout, this.trace, i, str4, this.bindCtls, str2, str1, localObject1, this.envprops);

                ...

//认证操作
localObject2 = this.clnt.authenticate(bool2, str1, localObject1, i, str4, this.bindCtls, this.envprops);

                ...

//认证失败的错误是从这里报的,但它并不是认证操作,而是根据上面认证操作返回的状态码报告错误而已。
//至于它为什么这么做,是因为即使认证失败,它还有后续的操作要做,比如关闭一个连接。
processReturnCode((LdapResult)localObject2);

                ...

可以看到,读证书的时候它并不是读取成一个字符串,而是读取成一个object类型,我们的认证失败可能根源于这里。那么接下来我们接近目的了,我们看到它的认证操作了,进去看看。先说明一下参数波bool2是clnt是否为空,这里必然为false,str1是登陆用户,localObject是证书,i是端口,str4是验证方式,我们指定为simple,事实上默认值也是这个值,后面两个一个就是环境变量的hashTable另一个我也布吉岛是什么。
这里对不同的验证方式进行了不同的操作,我们这里因为是采用simple方式验证,因此走的是下面这条流程:


                ...

/*      */       else if (paramString2.equalsIgnoreCase("simple"))
/*      */       {
/*  211 */         byte[] arrayOfByte = null;
/*      */         try {
                     //编码转换
/*  213 */           arrayOfByte = encodePassword(paramObject, this.isLdapv3);
                     //对ldap进行绑定
/*  214 */           localLdapResult = ldapBind(paramString1, arrayOfByte, paramArrayOfControl, null, false);
/*  215 */           if (localLdapResult.status == 0)
/*  216 */             this.conn.setBound();
/*      */         } catch (IOException localIOException4) {
/*      */           int j;
/*  219 */           localCommunicationException3 = new CommunicationException("simple bind failed: " + this.conn.host + ":" + this.conn.port);
/*      */           
/*      */ 
/*  222 */           localCommunicationException3.setRootCause(localIOException4);
/*  223 */           throw localCommunicationException3;
/*      */         }
/*      */         finally
/*      */         {
/*  227 */           if ((arrayOfByte != paramObject) && (arrayOfByte != null)) {
/*  228 */             for (int m = 0; m < arrayOfByte.length; m++) {
/*  229 */               arrayOfByte[m] = 0;
/*      */             }
/*      */           }
/*      */         }
/*  233 */       }  

encodePassword里面写了这些东西:

/*      */   private static byte[] encodePassword(Object paramObject, boolean paramBoolean)
/*      */     throws IOException
/*      */   {
/*  412 */     if ((paramObject instanceof char[])) {
/*  413 */       paramObject = new String((char[])paramObject);
/*      */     }
/*      */     
/*  416 */     if ((paramObject instanceof String)) {
/*  417 */       if (paramBoolean) {
/*  418 */         return ((String)paramObject).getBytes("UTF8");
/*      */       }
/*  420 */       return ((String)paramObject).getBytes("8859_1");
/*      */     }
/*      */     
/*  423 */     return (byte[])paramObject;
/*      */   }

它是判断你是不是v3版本的ldap,v3的字符编码和v2的不一样,因此进行了不同的编码转化,那么我们可以猜测,认证失败可能是因为版本的问题,等会儿如果我们还是找不到关键的问题所在的话我们应该试一下两个版本。

到这里解读结束,对于认证的解读有些简略,说实话这部分我也很混乱,大致的过程似乎是通过我们给定的账号去请求一个请求信息,然后看它怎么回应,如果成功就会回应一个ldap操作对象给我们,大概是这样吧。如果不去细究认证的细节的话,我们仅仅需要知道它尝试去认证了,之后获得一个保存结果的对象,在processReturnCode这个方法里面对这个状态码进行判断,认证成功状态码为0,我们这里认证失败,而且是用户名不正确则是抛出49的错误,如果认证失败则在这里负责把之前开启的资源关闭。其实如果我好好去看JNDI的文档或者好好细读ADS的文档大概就没这么多事了,懒人真的屎尿多。

<think>好的,用户需要关于LDAP初始化的代码示例或实现方法。首先,我需要确认LDAP的不同语言实现,常见的有Python、Java和PHP。用户可能对其中一种或多种语言感兴趣,所以最好覆盖这几个常用语言。 接下来,针对每个语言,回忆相关的库和初始化步骤。比如Python常用ldap3库,Java使用JNDI,PHP则是内置的ldap_connect函数。需要确保代码示例的正确性,语法正确,并且包含必要的步骤,如设置服务器地址、端口、绑定凭据等。 然后,检查是否需要处理安全连接,比如使用LDAPS或STARTTLS。在示例中加入相关参数,比如在Python中使用use_ssl=True,Java中设置环境属性,PHP中指定端口为636并启用TLS。这些细节能帮助用户避免常见的安全配置错误。 此外,用户可能关心错误处理和最佳实践,所以在代码中加入异常处理部分,比如try-catch块,说明可能的异常类型,这样用户知道如何调试和增强代码的健壮性。 还要考虑生成相关问题,比如LDAP初始化参数有哪些注意事项,如何配置安全连接,常见错误及解决方法。这些问题能够帮助用户进一步了解相关主题,解决可能遇到的问题。 最后,确保回答结构清晰,分语言说明,每个部分有代码示例和解释,引用来源如RFC文档或官方库文档,以增加可信度。同时检查是否符合用户之前的格式要求,比如使用中文,正确应用LaTeX格式(虽然这里可能用不上数学公式,但需留意),以及引用标识的位置。</think>### LDAP初始化实现方法 LDAP初始化通常包含服务器连接、身份验证、参数配置三个核心步骤,以下是不同语言的实现示例: #### Python示例(使用ldap3库) ```python from ldap3 import Server, Connection, ALL # 创建Server对象 server = Server('ldap://example.com:389', get_info=ALL) # 初始化连接(管理员绑定) conn = Connection(server, user='cn=admin,dc=example,dc=com', password='admin_password', auto_bind=True) # 执行搜索操作 conn.search('dc=example,dc=com', '(objectclass=person)') print(conn.entries) # 输出查询结果 ``` #### Java示例(使用JNDI) ```java import javax.naming.Context; import javax.naming.directory.InitialDirContext; import java.util.Hashtable; Hashtable<String, String> env = new Hashtable<>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://example.com:389"); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=admin,dc=example,dc=com"); env.put(Context.SECURITY_CREDENTIALS, "admin_password"); DirContext ctx = new InitialDirContext(env); // 初始化连接[^1] ``` #### PHP示例 ```php $ldapconn = ldap_connect("ldap://example.com") or die("无法连接LDAP服务器"); // 设置协议版本 ldap_set_option($ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3); // 绑定管理员账号 $ldapbind = ldap_bind($ldapconn, "cn=admin,dc=example,dc=com", "admin_password"); if ($ldapbind) { // 执行查询操作 $result = ldap_search($ldapconn, "dc=example,dc=com", "(sn=*)"); $entries = ldap_get_entries($ldapconn, $result); } ``` ### 关键参数说明 1. **协议版本**:现代LDAP服务建议使用v3协议 2. **连接模式**: - 明文连接:`ldap://` + 389端口 - 安全连接:`ldaps://` + 636端口 或 STARTTLS[^2] 3. **绑定凭证**:需要DN格式的用户名(如`cn=admin,dc=example,dc=com`)
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值