在分布式数据系统中,HBase常被部署于多租户的企业内网,直接暴露REST或者Thrift接口会带来凭据泄露风险。SPNEGO认证利用Kerberos票据,通过HTTP协议的标准协商机制,让客户端与服务端在无明文密码交换的前提下完成身份确认。本文围绕HBase的SPNEGO配置与代码对接,拆解从服务端启用到客户端调用的完整链路。

一、SPNEGO与Kerberos的关系
SPNEGO本身并不是一种新的加密算法,而是GSSAPI的协商包装层。在Kerberos环境中,应用通常通过GSSAPI获取服务票据,而SPNEGO把这种获取与校验过程映射到HTTP的WWW-Authenticate与Authorization头中。对于HBase来说,REST网关本质上是一个内嵌的Jetty服务,它借助Servlet过滤器识别 negotiate 类型的认证头,再调用Java的GSSManager完成校验。
很多工程师误以为启用了Kerberos的HBase就自动支持浏览器单点登录,其实两者中间缺了SPNEGO这一层协议转换。没有它,RegionServer之间的RPC虽然安全,但对外HTTP接口仍然要么匿名,要么依赖其他薄弱的认证方式。只有把HTTP主体的Kerberos principal注册到KDC,并在服务端开启SPNEGO,外部调用才会被正确识别为合法票据携带者。
二、服务端开启HBase SPNEGO
首先在KDC中为HTTP服务创建principal,例如 HTTP/hbase-host@EXAMPLE.COM,并导出keytab到各节点。随后修改 hbase-site.xml,打开SPNEGO开关并声明主体。下面是一段典型的配置片段,注意其中的键值均使用小写横线风格,且principal需和keytab中的名称完全一致。
<configuration>
<property>
<name>hbase.security.authentication</name>
<value>kerberos</value>
</property>
<property>
<name>hbase.security.authentication.spnego.kerberos.principal</name>
<value>HTTP/hbase-host@EXAMPLE.COM</value>
</property>
<property>
<name>hbase.security.authentication.spnego.kerberos.keytab</name>
<value>/etc/security/keytabs/spnego.service.keytab</value>
</property>
<property>
<name>hbase.rest.authentication.type</name>
<value>kerberos</value>
</property>
</configuration>
配置完成后重启HBase Master与RegionServer,以及REST服务进程。此时若用 curl 不带认证头访问,会收到 401 与 WWW-Authenticate: Negotiate 的响应,说明服务端已具备SPNEGO握手能力。这种返回是标准行为,代表服务端在等待客户端提交GSSAPI令牌。
需要提醒的是,REST与Thrift网关的SPNEGO开关是独立控制的。如果只改了 hbase-site 却没有在网关启动参数中引入对应认证过滤器,仍然无法生效。生产环境建议统一通过配置管理工具分发keytab,避免权限错位导致GSSException。
三、客户端如何发起SPNEGO请求
最简单的验证方式是使用已获得Kerberos票据的会话,配合支持Negotiate的HTTP工具。例如Linux下先执行 kinit,再用 curl --negotiate 访问。但编程场景中往往需要在Java应用内构造请求,这时要使用GSSCredential与HTTP连接配合。
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Base64;
public class SpnegoClient {
public static void main(String[] args) throws Exception {
// 定义SPNEGO的Oid
Oid spnegoOid = new Oid("1.3.6.1.5.5.2");
GSSManager manager = GSSManager.getInstance();
// 服务主体格式为 HTTP@host
GSSName serverName = manager.createName("HTTP@hbase-host", null);
GSSContext context = manager.createContext(serverName, spnegoOid, null, GSSContext.DEFAULT_LIFETIME);
context.requestMutualAuth(true);
byte[] token = context.initSecContext(new byte[0], 0, 0);
String negotiate = Base64.getEncoder().encodeToString(token);
URL url = new URL("http://hbase-host:8080/");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Authorization", "Negotiate " + negotiate);
int code = conn.getResponseCode();
System.out.println("HTTP状态码:" + code);
context.dispose();
}
}
上述代码演示了如何手动生成初始令牌并塞入Authorization头。在实际框架如Apache HttpClient中,可使用KerberosRestTemplate或相应的AuthScheme来免去底层GSS调用。但理解这段逻辑有助于排查如“令牌过期”“principal不匹配”等典型错误。
当客户端与服务端处于不同域或跨KDC信任链时,SPNEGO握手可能在中途要求二次令牌交换。代码里如果只发送第一次initSecContext的结果,会收到 401 并携带后续挑战,此时需要循环调用 take method 直到 isEstablished 为 true,再把最终上下文产生的令牌随业务请求发出。
四、常见故障与排查思路
第一类问题是KDC中缺少HTTP主体,报错多为“No valid credentials provided”。用 klist 检查票据缓存,确认含 HTTP/hbase-host 的服务票据。第二类是keytab权限过宽,Java进程因无法读取而抛出 IOException,应保证文件属主与启动用户一致且权限为600。
第三类隐蔽故障来自主机名规范。SPNEGO严重依赖反向DNS,若客户端填的是IP而principal登记的是域名,GSSName解析会失败。建议在URL与principal中都使用同一套FQDN。通过开启Sun的调试参数 -Dsun.security.krb5.debug=true 可看到完整协商轨迹,快速定位是哪一步校验被拒。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 401持续返回 | 客户端未kinit或票据失效 | 重新获取票据并确认生命周期 |
| GSSException: Defective token | Authorization头未Base64或拼错前缀 | 检查Negotiate空格与编码 |
| 时钟偏移报错 | 节点间NTP不同步 | 统一时间服务 |
五、与网关集成的建议
在大型集群前通常会部署Knox或Nginx做统一入口。此时后端HBase只需信任网关转发的SPNEGO头,或者网关自身终结Kerberos后再以受信身份调用HBase。前者要求网关透传 negotiate 令牌,后者则把网关当作唯一SPNEGO端点,降低各节点直接暴露的风险。
从运维角度看,把SPNEGO集中到网关能减少keytab分发点,也便于做审计日志。但若业务要求端到端加密身份,则必须让HBase自身完成校验,网关仅做路由。两种模式各有取舍,团队应基于合规与延迟要求做决定,而不是默认全量开启服务端SPNEGO。