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的认证过程
-
usergroupinformation.loginuserfromkeytab 利用传入的user和keytab路径信息,构建一个logincontext,接着调用logincontext的login方法
try { login = newlogincontext(hadoopconfiguration.keytab_kerberos_config_name, subject, new hadoopconfiguration()); start = time.now(); login.login(); 。。。
-
logincontext.login方法依次通过反射调用了登陆模块的login和commit两个方法,调用的主要逻辑在invokepriv方法内
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
- hadooploginmodule :和kerberos认证关系不大,暂且不看
- krb5loginmodule : kerberos认证类,根据第2步logincontext.login中的方法可知,会依次调用这个module中的login和commit两个方法
- krb5loginmodule.login方法,就是利用我们提供的user名称和krb5.conf中的配置信息以及keytab信息进行认证。代码就不展示了,主要是调用attemptauthentication进行的处理。
-
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"); } 。。。
-
按照这个逻辑,既然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结合后,出了错还是有些难调查,可只要慢慢分析,还是会找到解决方法的,还有一点就是虽然程序出现的错误一样,引起错误的根本原因还是会有所不同,不能只是按照网上说法一改就万事大吉,有时还是需要靠我们自己刨根问底好好研究。
上一篇: 企业官方微博营销技巧分享