主要是因为犯了一个很愚蠢的错误,在实例化 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)throwsNamingException
- {
- myProps = (Hashtable)
- ResourceManager.getInitialEnvironment(environment);
- if(myProps.get(Context.INITIAL_CONTEXT_FACTORY) !=null) {// user has specified initial context factory; try to get itgetDefaultInitCtx();
- }
- }
在 getDefaultInitCtx 调用 NamingManager 的 getInitialContext,我们应该认为这才是制造 DirContext 的工厂。在这里进行了如下操作:
- ...//获取工厂类名
- String className = env != null ?
- (String)env.get(Context.INITIAL_CONTEXT_FACTORY) : null;...//获得工厂类实例
- factory = (InitialContextFactory)
- helper.loadClass(className).newInstance();...//通过工厂类构建上下文returnfactory.getInitialContext(env);
在 getInitialContext 中主要操作如下:
- ...//获取ldapURL
- String str = paramHashtable != null ? (String)paramHashtable.get("java.naming.provider.url") : null;...//切割成string数组
- arrayOfString = LdapURL.fromList(str);...//获取LdapCtx实例returngetLdapCtxInstance(arrayOfString, paramHashtable);
getLdapCtxInstance 如下:
- {if((paramObjectinstanceof String))returngetUsingURL((String)paramObject, paramHashtable);if((paramObjectinstanceof String[])) {returngetUsingURLs((String[])paramObject, paramHashtable);
- }
因为我们是数组,所以这里调用的是第二个函数:
- ...
- for(int i =0; i < paramArrayOfString.length; i++) {try{returngetUsingURL(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 的文档大概就没这么多事了,懒人真的屎尿多。
来源: http://blog.csdn.net/jiujiuming/article/details/70145165