關於hadoop登陸kerberos時設定環境變數問題的思考
中心思想,設定kerberos環境變數時,發現JDK原始碼當中的一個問題,故描述如下。
在平時的使用中,如果hadoop叢集配置kerberos認證的話,使用java訪問hdfs或者hive時,需要先進行認證登陸,之後才可以訪問。登陸程式碼大致如下:
package demo.kerberos; import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.security.UserGroupInformation; public class KerberosLoginTest { public static void main(String[] args) throws IOException { // 設定kerberos相關登陸資訊 // 方法1:設定krb5.conf配置檔案 System.setProperty("java.security.krb5.conf", "/home/eabour/hadoop/conf/krb5.conf"); // 方法2:設定kerberos環境變數 // System.setProperty("java.security.krb5.realm", "HADOOP.COM"); // System.setProperty("java.security.krb5.kdc", "kdc.server.com"); // 開啟登陸除錯日誌 System.setProperty("sun.security.krb5.debug", "true"); Configuration conf = new Configuration(); conf.addResource(new Path("/home/eabour/hadoop/conf/core-site.xml")); conf.addResource(new Path("/home/eabour/hadoop/conf/hdfs-site.xml")); UserGroupInformation.setConfiguration(conf); // 登陸kerberos UserGroupInformation.loginUserFromKeytab("[email protected]", "/home/eabour/test.keytab"); // TODO // 訪問HDFS } }
設定krb5.conf和core-site.xml、hdfs-site.xml相關大資料平臺的配置檔案,用來初始化相關配置資訊。使用krb5.conf登陸沒有問題,不管設定多個kdc server還是單個,jdk中的相關模組都會解析出來,jdk原始碼位置為openjdk\jdk\src\share\classes\sun\security\krb5,其中openjdk的原始碼地址為http://hg.openjdk.java.net/jdk7u/jdk7u60/jdk/file/33c1eee28403/src/share/classes,其他版本的類似,解析類為sun.security.krb5.Config,部分程式碼如下:
/** * Private constructor - can not be instantiated externally. */ private Config() throws KrbException { /* * If either one system property is specified, we throw exception. */ String tmp = getProperty("java.security.krb5.kdc"); if (tmp != null) { // The user can specify a list of kdc hosts separated by ":" defaultKDC = tmp.replace(':', ' '); } else { defaultKDC = null; } defaultRealm = getProperty("java.security.krb5.realm"); if ((defaultKDC == null && defaultRealm != null) || (defaultRealm == null && defaultKDC != null)) { throw new KrbException ("System property java.security.krb5.kdc and " + "java.security.krb5.realm both must be set or " + "neither must be set."); } // Always read the Kerberos configuration file try { Vector<String> configFile; String fileName = getJavaFileName(); if (fileName != null) { configFile = loadConfigFile(fileName); stanzaTable = parseStanzaTable(configFile); if (DEBUG) { System.out.println("Loaded from Java config"); } } else { boolean found = false; if (isMacosLionOrBetter()) { try { stanzaTable = SCDynamicStoreConfig.getConfig(); if (DEBUG) { System.out.println("Loaded from SCDynamicStoreConfig"); } found = true; } catch (IOException ioe) { // OK. Will go on with file } } if (!found) { fileName = getNativeFileName(); configFile = loadConfigFile(fileName); stanzaTable = parseStanzaTable(configFile); if (DEBUG) { System.out.println("Loaded from native config"); } } } } catch (IOException ioe) { // No krb5.conf, no problem. We'll use DNS or system property etc. } }
從程式碼可以看出,先從環境變數中獲取java.security.krb5.kdc和java.security.krb5.realm,然後在讀取配置檔案java.security.krb5.conf。如果同時設定java.security.krb5.kdc、java.security.krb5.realm和java.security.krb5.conf,在登陸的時候,會使用java.security.krb5.kdc,除非登陸的realm與defaultRealm不一致,從程式碼中可以看出來。
那麼,我要說的問題來了,在某些場景下,沒有設定java.security.krb5.conf變數,而使用java.security.krb5.kdc、java.security.krb5.realm來登陸。在1個或多個kdc server情況下,這樣的設定沒有問題。我們知道kdc server的預設埠為88,使用的時UDP協議,當然可以為TCP協議,服務埠也可以修改,這時候大致為:
kdc=kdc.server.com:88
或者
kdc=kdc.server.com:1088
krb5.conf檔案配置
[realms] HADOOP.COM = { kdc = kdc.server.com:1088 }
此時,設定環境變數:
System.setProperty("java.security.krb5.kdc", "kdc.server.com:1088");
那麼,真正的問題就來了,執行登陸的話,會出現如下報錯:
Caused by: GSSException: No valid credentials provided (Mechanism level: ICMP Port Unreachable) at sun.security.jgss.krb5.Krb5Context.initSecContext(Krb5Context.java:775) 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:192) … 76 more Caused by: java.net.PortUnreachableException: ICMP Port Unreachable at java.net.PlainDatagramSocketImpl.receive0(Native Method) at java.net.AbstractPlainDatagramSocketImpl.receive(AbstractPlainDatagramSocketImpl.java:143) at java.net.DatagramSocket.receive(DatagramSocket.java:812) at sun.security.krb5.internal.UDPClient.receive(NetClient.java:206) at sun.security.krb5.KdcComm$KdcCommunication.run(KdcComm.java:411) at sun.security.krb5.KdcComm$KdcCommunication.run(KdcComm.java:364) at java.security.AccessController.doPrivileged(Native Method) at sun.security.krb5.KdcComm.send(KdcComm.java:348) at sun.security.krb5.KdcComm.sendIfPossible(KdcComm.java:253) at sun.security.krb5.KdcComm.send(KdcComm.java:229) at sun.security.krb5.KdcComm.send(KdcComm.java:200) at sun.security.krb5.KrbTgsReq.send(KrbTgsReq.java:254) at sun.security.krb5.KrbTgsReq.sendAndGetCreds(KrbTgsReq.java:269) at sun.security.krb5.internal.CredentialsUtil.serviceCreds(CredentialsUtil.java:302) at sun.security.krb5.internal.CredentialsUtil.acquireServiceCreds(CredentialsUtil.java:120) at sun.security.krb5.Credentials.acquireServiceCreds(Credentials.java:458) at sun.security.jgss.krb5.Krb5Context.initSecContext(Krb5Context.java:693)
為什麼會報錯呢?因為在解析環境變數時,解析出問題了,現在,再來看看解析程式碼:
/** * Private constructor - can not be instantiated externally. */ private Config() throws KrbException { /* * If either one system property is specified, we throw exception. */ String tmp = getProperty("java.security.krb5.kdc"); if (tmp != null) { // The user can specify a list of kdc hosts separated by ":" defaultKDC = tmp.replace(':', ' '); } else { defaultKDC = null; } defaultRealm = getProperty("java.security.krb5.realm"); if ((defaultKDC == null && defaultRealm != null) || (defaultRealm == null && defaultKDC != null)) { throw new KrbException ("System property java.security.krb5.kdc and " + "java.security.krb5.realm both must be set or " + "neither must be set."); }
請看紅色黃底的程式碼 defaultKDC = tmp.replace(':', ' ') ,對,就是這句程式碼的問題,他將kdc.server.com:1088分割為kdc.server.com和1088了,認為時兩個kdc server。我的心崩潰呀,為什麼要用冒號來分割多個server的配置,如果在使用預設埠的話,這樣也沒問題,但是,如果kdc修改了埠,這種通過環境變數設定kdc server的方式就沒法用了。
到最後,原來時jdk原始碼的問題,正常的途徑怕是設定不了了。但是,不是不能修改了,我們可以用反射來修改Config的defaultKDC的值,雖然說defaultKDC為final String ,到那時它是private final String defaultKDC;,所以還是可以修改的。
以上就是我分析的問題,在實際專案中是真實遇到過,因為涉及JDK底層程式碼,所以請大家來參詳一下。
附:
1.kerberos配置檔案設定:https://www.ibm.com/support/knowledgecenter/zh/SSAW57_9.0.0/com.ibm.websphere.nd.multiplatform.doc/ae/tsec_kerb_create_conf.html