DirContext初始化过程分析

时间:2022-06-09 03:51:39

这篇博文的原因

主要是因为犯了一个很愚蠢的错误,在实例化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的文档大概就没这么多事了,懒人真的屎尿多。