hdfs/hbase 程序利用Kerberos認證超過ticket_lifetime期限后異常


問題描述

業務需要一個長期運行的程序,將上傳的文件存放至HDFS,程序啟動后,剛開始一切正常,執行一段時間(一般是一天,有的現場是三天),就會出現認證錯誤,用的JDK是1.8,hadoop-client,對應的版本是2.5.1,為什么強調這個版本號,因為錯誤的根本原因就是由JDK版本和hadoop-client版本不匹配引起的。

錯誤日志

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過期

  • 應用在每次獲取FileSystem時,都會先調用kerberosLogin,之后才獲取FileSystem

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的認證過程

  1. UserGroupInformation.loginUserFromKeytab 利用傳入的user和keytab路徑信息,構建一個LoginContext,接着調用LoginContext的login方法

    
     try {
          login = newLoginContext(HadoopConfiguration.KEYTAB_KERBEROS_CONFIG_NAME,
                subject, new HadoopConfiguration());
          start = Time.now();
          login.login();
        
        ...
        
    
  2. LoginContext.login方法依次通過反射調用了登陸模塊的login和commit兩個方法,調用的主要邏輯在invokePriv方法內

    
    public void login() throws LoginException {
    
        ...
    
        try {
            // module invoked in doPrivileged
            invokePriv(LOGIN_METHOD);
            invokePriv(COMMIT_METHOD);
    
    ...
    
    
  3. 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();
                                
    
  4. 實際執行時對應的moduleStack中有兩個LoginModule

    • HadoopLoginModule :和kerberos認證關系不大,暫且不看

    • Krb5LoginModule : kerberos認證類,根據第2步LoginContext.login中的方法可知,會依次調用這個module中的login和commit兩個方法

      1. Krb5LoginModule.login方法,就是利用我們提供的user名稱和krb5.conf中的配置信息以及keytab信息進行認證。代碼就不展示了,主要是調用attemptAuthentication進行的處理。
      2. Krb5LoginModule.commit方法是要把認證后證書信息存入到Subject中,以便后續能重復使用subject進行認證,和本次調查問題有關的代碼片段如下
      
      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");
                }
               
        ...
      
      
  5. 按照這個邏輯,既然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();
      }
    
    
  6. 方法邏輯,就是判斷如果是用keytab進行的認證,就調用reloginFromKeytab進行認證。但在實際執行時卻發現isKeytab的值是false,可代碼明明是使用keytab來認證的,怎么是false呢,只能看看isKeytab這個值怎么賦值的了,對應邏輯在UserGroupInformation的構造函數里

    	
    	UserGroupInformation(Subject subject) {
    	    ...
    	    this.isKeytab = !subject.getPrivateCredentials(KerberosKey.class).isEmpty();
    	    ...
      	}
    
    
  7. 至此終於發現問題所在,我們在第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();
    }
    
    
  8. 可以看到判斷對象已經變成了KeyTab了,並且從注釋信息中明確看到在JDK7時使用的是KerberosKey,在JDK8時換成了KeyTab。

總結,kerberos認證功能雖然強大,實際使用還是有點復雜,特別是和jaas結合后,出了錯還是有些難調查,可只要慢慢分析,還是會找到解決方法的,還有一點就是雖然程序出現的錯誤一樣,引起錯誤的根本原因還是會有所不同,不能只是按照網上說法一改就萬事大吉,有時還是需要靠我們自己刨根問底好好研究。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM