• hdfs/hbase 程序利用Kerberos认证超过ticket_lifetime期限后异常


    问题描述

    业务需要一个长期运行的程序,将上传的文件存放至HDFS,程序启动后,刚开始一切正常,执行一段时间(一般是一天,有的现场是三天),就会出现认证错误,用的JDK是1.8,hadoop-client,对应的版本是2.5.1,为什么强调这个版本号,因为错误的根本原因就在于版本问题

    错误日志

    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();

    。。。
    ```
    
    1. LoginContext.login方法依次通过反射调用了登陆模块的login和commit两个方法,调用的主要逻辑在invokePriv方法内

      public void login() throws LoginException {
      
          ...
      
          try {
              // module invoked in doPrivileged
              invokePriv(LOGIN_METHOD);
              invokePriv(COMMIT_METHOD);
      
      ...
      
    2. 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
    
    * HadoopLoginModule :和kerberos认证关系不大,暂且不看
    * Krb5LoginModule : kerberos认证类,根据第2步LoginContext.login中的方法可知,会依次调用这个module中的login和commit两个方法
    
    1. Krb5LoginModule.login方法,就是利用我们提供的user名称和krb5.conf中的配置信息以及keytab信息进行认证。代码就不展示了,主要是调用attemptAuthentication进行的处理。  
    1. 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");
                      }
                     
    	 		。。。
    	 	
    	```
    1. 按照这个逻辑,既然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();
      	    ...
        	}
    	
    	```
    1. 至此终于发现问题所在,我们在第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结合后,出了错还是有些难调查,可只要慢慢分析,还是会找到解决方法的,还有一点就是虽然程序出现的错误一样,引起错误的根本原因还是会有所不同,不能只是按照网上说法一改就万事大吉,有时还是需要靠我们自己刨根问底好好研究。
  • 相关阅读:
    [转]C# 动态调用 WebService
    [转]走进 LINQ 的世界
    [转]mybatis-generator 代码自动生成工具(maven方式)
    [转]Netty入门(最简单的Netty客户端/服务器程序)
    FastJson 常见问题
    初识 ElasticSearch
    Maven Gradle 区别
    IDEA 自动生成serialVersionUID
    restful 架构详解
    初识shell expect
  • 原文地址:https://www.cnblogs.com/moon3/p/14040276.html
Copyright © 2020-2023  润新知