業務須要一個長期運行的程序,將上傳的文件存放至HDFS,程序啓動後,剛開始一切正常,執行一段時間(通常是一天,有的現場是三天),就會出現認證錯誤,用的JDK是1.8,hadoop-client,對應的版本是2.5.1,爲何強調這個版本號,由於錯誤的根本緣由就在於版本問題java
Caused by: org.ietf.jgss.GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt) at sun.security.jgss.krb5.Krb5InitCredential.getInstance(Krb5InitCredential.java:147) ~[?:1.8.0_212] at sun.security.jgss.krb5.Krb5MechFactory.getCredentialElement(Krb5MechFactory.java:122) ~[?:1.8.0_212] at sun.security.jgss.krb5.Krb5MechFactory.getMechanismContext(Krb5MechFactory.java:187) ~[?:1.8.0_212] at sun.security.jgss.GSSManagerImpl.getMechanismContext(GSSManagerImpl.java:224) ~[?:1.8.0_212] at sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:212) ~[?:1.8.0_212] at sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:179) ~[?:1.8.0_212] at com.sun.security.sasl.gsskerb.GssKrb5Client.evaluateChallenge(GssKrb5Client.java:192) ~[?:1.8.0_212] at org.apache.hadoop.security.SaslRpcClient.saslConnect(SaslRpcClient.java:413) ~[hadoop-common-2.5.1.jar:?] at org.apache.hadoop.ipc.Client$Connection.setupSaslConnection(Client.java:552) ~[hadoop-common-2.5.1.jar:?] at org.apache.hadoop.ipc.Client$Connection.access$1800(Client.java:367) ~[hadoop-common-2.5.1.jar:?] at org.apache.hadoop.ipc.Client$Connection$2.run(Client.java:717) ~[hadoop-common-2.5.1.jar:?] at org.apache.hadoop.ipc.Client$Connection$2.run(Client.java:713) ~[hadoop-common-2.5.1.jar:?] at java.security.AccessController.doPrivileged(Native Method) ~[?:1.8.0_212] at javax.security.auth.Subject.doAs(Subject.java:422) ~[?:1.8.0_212] at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1614) ~[hadoop-common-2.5.1.jar:?] at org.apache.hadoop.ipc.Client$Connection.setupIOstreams(Client.java:712) ~[hadoop-common-2.5.1.jar:?] at org.apache.hadoop.ipc.Client$Connection.access$2800(Client.java:367) ~[hadoop-common-2.5.1.jar:?] at org.apache.hadoop.ipc.Client.getConnection(Client.java:1463) ~[hadoop-common-2.5.1.jar:?] at org.apache.hadoop.ipc.Client.call(Client.java:1382) ~[hadoop-common-2.5.1.jar:?] ... 61 more
public void init() { System.setProperty("java.security.krb5.conf", "krb5.conf"); } public void kerberosLogin() throws IOException { // 已經認證經過 if ("hdfsuser".concat("@").concat("DATAHOUSE.COM") .equals(UserGroupInformation.getCurrentUser().getUserName())) { UserGroupInformation.getCurrentUser().checkTGTAndReloginFromKeytab(); return; } // ksbName 表示用戶名 keytabPath表示祕鑰存放位置 UserGroupInformation.loginUserFromKeytab("hdfsuser", "/etc/keytab/hdfsuser.keytab"); }
主要思想就是第一次認證經過loginUserFromKeytab進行認證,以後每次請求再調用checkTGTAndReloginFromKeytab方法判斷是否須要從新認證,防止ticket過時apache
應用在每次獲取FileSystem時,都會先調用kerberosLogin,以後才獲取FileSystemide
public FileSystem getFileSystem() throws IOException { try { kerberosLogin(); return FileSystem.get(configuration); } catch (Exception e) { logger.error("create hdfs FileSystem has error", e); throw e; } }
根據錯誤在網上各類搜索,出來的結果和上面的代碼大同小異,有的猜想是客戶端調用間隔太大,超過了ticket_lifetime的值,建議加一個定時任務來週期性的調用kerberosLogin()方法,雖然咱們業務不太可能出現這種狀況,仍是加上了這個處理,問題依舊,只好開始慢慢調試函數
UserGroupInformation.loginUserFromKeytab的認證過程oop
UserGroupInformation.loginUserFromKeytab 利用傳入的user和keytab路徑信息,構建一個LoginContext,接着調用LoginContext的login方法this
try { login = newLoginContext(HadoopConfiguration.KEYTAB_KERBEROS_CONFIG_NAME, subject, new HadoopConfiguration()); start = Time.now(); login.login(); 。。。
LoginContext.login方法依次經過反射調用了登錄模塊的login和commit兩個方法,調用的主要邏輯在invokePriv方法內lua
public void login() throws LoginException { ... try { // module invoked in doPrivileged invokePriv(LOGIN_METHOD); invokePriv(COMMIT_METHOD); ...
LoginContext.invokePriv方法主要在doPrivileged內調用invoke方法,invoke方法依次調用登錄模塊對應的方法,第一次調用時,還會調用對應的initialize方法調試
for (int i = moduleIndex; i < moduleStack.length; i++, moduleIndex++) { try { ... // 查找initialize方法 methods = moduleStack[i].module.getClass().getMethods(); for (mIndex = 0; mIndex < methods.length; mIndex++) { if (methods[mIndex].getName().equals(INIT_METHOD)) { break; } } Object[] initArgs = {subject, callbackHandler, state, moduleStack[i].entry.getOptions() }; // 調用 initialize 方法 methods[mIndex].invoke(moduleStack[i].module, initArgs); } // 接着查找相應的方法 for (mIndex = 0; mIndex < methods.length; mIndex++) { if (methods[mIndex].getName().equals(methodName)) { break; } } // set up the arguments to be passed to the LoginModule method Object[] args = { }; // 調用相應的方法 boolean status = ((Boolean)methods[mIndex].invoke (moduleStack[i].module, args)).booleanValue();
實際執行時對應的moduleStack中有兩個LoginModule日誌
Krb5LoginModule.commit方法是要把認證後證書信息存入到Subject中,以便後續能重複使用subject進行認證,和本次調查問題有關的代碼片斷以下code
public boolean commit() throws LoginException { Set<Object> privCredSet = subject.getPrivateCredentials(); 。。。 if (ktab != null) { if (!privCredSet.contains(ktab)) { // 把keytab保存下來,再次認證使用 privCredSet.add(ktab); } } else { succeeded = false; throw new LoginException("No key to store"); } 。。。
按照這個邏輯,既然keytab保存到Subject中了,再次使用UserGroupInformation.getCurrentUser().checkTGTAndReloginFromKeytab();進行認證時,就可使用保存的keytab直接認證了,應該是不會出錯的,咱們看下checkTGTAndReloginFromKeytab方法
public synchronized void checkTGTAndReloginFromKeytab() throws IOException { if (!isSecurityEnabled() || user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS || !isKeytab) return; KerberosTicket tgt = getTGT(); if (tgt != null && Time.now() < getRefreshTime(tgt)) { return; } reloginFromKeytab(); }
方法邏輯,就是判斷若是是用keytab進行的認證,就調用reloginFromKeytab進行認證。但在實際執行時卻發現isKeytab的值是false,可代碼明明是使用keytab來認證的,怎麼是false呢,只能看看isKeytab這個值怎麼賦值的了,對應邏輯在UserGroupInformation的構造函數裏
UserGroupInformation(Subject subject) { ... this.isKeytab = !subject.getPrivateCredentials(KerberosKey.class).isEmpty(); ... }
至此終於發現問題所在,咱們在第5步,認證成功後在subject的PrivateCredentials中存入的是keytab對象,而這個地方判斷的是KerberosKey,這確定是不同呀,那就只有一種可能,就是引用jar包的版本問題了。更換hadoop-client的版本號爲2.10.0,再查看UserGroupInformation對應的構造函數
private UserGroupInformation(Subject subject, final boolean externalKeyTab) { ... this.isKeytab = KerberosUtil.hasKerberosKeyTab(subject); ... }
將判斷邏輯移到了KerberosUtil.hasKerberosKeyTab方法中
/** * Check if the subject contains Kerberos keytab related objects. * The Kerberos keytab object attached in subject has been changed * from KerberosKey (JDK 7) to KeyTab (JDK 8) * * * @param subject subject to be checked * @return true if the subject contains Kerberos keytab */ public static boolean hasKerberosKeyTab(Subject subject) { return !subject.getPrivateCredentials(KeyTab.class).isEmpty(); }
能夠看到判斷對象已經變成了KeyTab了,而且從註釋信息中明確看到在JDK7時使用的是KerberosKey,在JDK8時換成了KeyTab。
總結,kerberos認證功能雖然強大,實際使用仍是有點複雜,特別是和jaas結合後,出了錯仍是有些難調查,可只要慢慢分析,仍是會找到解決方法的,還有一點就是雖然程序出現的錯誤同樣,引發錯誤的根本緣由仍是會有所不一樣,不能只是按照網上說法一改就萬事大吉,有時仍是須要靠咱們本身刨根問底好好研究。