在理清SASL/GSSAPI/Kerberos文中,咱们理解到,在java中能够通过Krb5LoginModule
模块实现Kerberos登录,以及通过GSSAPI
实现校验的根本流程;同时也明确java还定义了一个高层次接口SASL
形象校验流程。校验流程并没有定义通过何种通信形式传递“票据”,用户能够本人开发适合的通信计划,而SPNEGO
协定就是基于http传递票据和校验的规范计划,由微软提出,SPNEGO
宽泛用于心愿集成kerberos认证的并且基于http的服务中。本文基于spring interceptor
尝试实现一个繁难的SPNEGO
服务。
筹备工作
咱们须要筹备一些测试所以须要的环境和物料。首先咱们先回顾一下基于kerberos认证拜访服务的整个过程:
1来源gaodai#ma#com搞*代#码网-2: 客户端首先从KDC中验证失去票据,基于客户端持有的用户名(Principal)和明码(keytab)
3-4: 客户端从KDC上获取要拜访的服务(SerivcePrincipal)的票据
5-6: 客户端拜访服务(SerivcePrincipal)的时候携带票据,服务端校验票据的合法性
从这个流程能够发现,这是一个爱护服务端的流程,也就是说不是谁都能轻易拜访某服务的,必须是持有非法用户名(Principal)和明码(keytab)的客户端。当然,客户端和服务端的通信还能够再多进一步,即客户端验证服务端的合法性,但这个流程个别是省略的。
从这个流程看,咱们须要如下环境和物料:
- KDC服务器,以及这个服务器的地址信息配置(
krb5.conf
),用于Krb5LoginModule从KDC处获取票据 - 一个非法的客户端用户名(
Principal
)和明码(keytab
) - 一个非法的服务端
SerivcePrincipal
,服务端在构建的时候也须要登录SerivcePrincipal
,所以SerivcePrincipal
对应的keytab也须要
SPNEGO
SPNEGO
协定只是在kerberos的流程的根底上,将上述的5-6步,通过http的形式定义了。咱们能够通过curl
来拜访kerberos认证的服务,例如:
curl -u : -i -k --negotiate 'https://192.168.21.134:24148/
其中--negotiate
通知curl反对SPNEGO
协定。咱们来剖析一下curl的流程:
- 发送一般http申请
- 服务端返回401和
WWW-Authentication: Negotiation
头 - curl会初始化gss\_context,开始kerberos认证流程
- 以以后kinit登录的用户为客户端用户,从kdc处获取票据
- 以HTTP/host@DOMAIN为
SerivcePrincipal
,从kdc处取得该服务的拜访票据。这里的host是被拜访的服务器地址或主机名,DOMAIN为kdc的默认域名。这个规定是curl固定的。所以这就象征两点:1.SerivcePrincipal
必须是存在的,2. 服务端也须要在以SerivcePrincipal
登录的上下文中验证客户端的票据 - 再次发送http申请,生成Authentication: Negotiation xxxxxxx,其中xxxxx为加工当前的base64编码字符串
- 服务端验证xxxxxxx
代码实现
理解了基本原理后,着手开发,spring-security-kerberos这个我的项目基于spring security框架实现了kerberos认证的服务。受这个我的项目启发,咱们用interceptor
实现:
<code class="java">@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getRequestURI().contains("/api/v1/xxl-job")) { return true; } if (!initialized) { try { // TODO refreshable LoginConfig loginConfig = new LoginConfig( config.getSpnegoAuthConfig().getKeytab(), config.getSpnegoAuthConfig().getPrincipal(), Boolean.TRUE.equals(config.getSpnegoAuthConfig().getDebug()) ); Subject sub = new Subject(); loginContext = new LoginContext("", sub, (CallbackHandler)null, loginConfig); loginContext.login(); } catch (Exception ex) { logger.error("Failed to initialize GSSAPI context", ex); loginContext = null; } initialized = true; } if (loginContext == null) { // 如果曾经初始化过,然而失败了,则放行 return true; } String auth = request.getHeader(AUTHORIZATION); if (auth != null) { String userName = Subject.doAs(loginContext.getSubject(), new AuthAction(auth.trim())); if (userName != null) { logger.debug("Login user by spnego: {}", userName); return true; } } commence(response); return false; } private void commence(HttpServletResponse response) throws IOException { response.addHeader("WWW-Authenticate", "Negotiate"); // NOTE: 不能写成sendError,会返回两个WWW-Authenticate: Negotiate。why? // response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); response.setStatus(401); response.flushBuffer(); }
咱们晓得preHandle
会拦挡任何一个申请,在这外面咱们初始化一次logContext
,这里应用的是ServicePrincipal,而且必须是如下模式:
HTTP/<HOST>@<REAL_DOMAIN>
试图获取申请头中的Authorization
String auth = request.getHeader(AUTHORIZATION);
如果存在,就进入验证逻辑,如果不存在或者验证失败,就返回401和WWW-Authenticate: Negotiate
以上是代码的根本逻辑,重点是看一下验证逻辑:
<code class="java">public class AuthAction implements PrivilegedExceptionAction<String> { private String authString; public AuthAction(String authString) { this.authString = authString; } @Override public String run() throws Exception { GSSContext context = GSSManager.getInstance().createContext((GSSCredential)null); try { String token = authString.substring("Negotiate".length()).trim(); byte[] kerberosTicket = java.util.Base64.getDecoder().decode(token); context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length); return context.getSrcName().toString(); } catch (Exception ex) { logger.error("Failed to auth token", ex); return null; } finally { context.dispose(); } } }
能够分明的看到,代码是如何解码base64,以及最终调用理清SASL/GSSAPI/Kerberos文中提到的acceptSecContext
的。
还有一个技巧,是咱们本人实现了LoginConfig
类,而不依赖jaas配置文件,因为jdk实现的基于jaas配置文件登录的机制,须要应用System.setProperty
配置,会净化全局环境。自定义Config类后,能够决定如何将登录的配置信息传递给LoginModule
,而不局限于应用全局配置项。受spring-security-kerberos启发,LoginConfig
的实现如下:
<code class="java">public class LoginConfig extends Configuration { private String keyTabLocation; private String servicePrincipalName; private boolean debug; public LoginConfig(String keyTabLocation, String servicePrincipalName, boolean debug) { this.keyTabLocation = keyTabLocation; this.servicePrincipalName = servicePrincipalName; this.debug = debug; } public AppConfigurationEntry[] getAppConfigurationEntry(String name) { HashMap<String, String> options = new HashMap(); options.put("useKeyTab", "true"); options.put("keyTab", this.keyTabLocation); options.put("principal", this.servicePrincipalName); options.put("storeKey", "true"); options.put("doNotPrompt", "true"); if (this.debug) { options.put("debug", "true"); } options.put("isInitiator", "false"); return new AppConfigurationEntry[]{ new AppConfigurationEntry( "com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) }; } }
启动服务
启动调试服务的时候,必须减少-Djava.security.krb5.conf=xxxxx
,还能够减少如下配置项辅助问题诊断:
-Dsun.security.krb5.debug=true
-Dsun.security.spnego.debug=true
遇到的问题和解决
Failure unspecified at GSS-API level (Mechanism level: Invalid argument (400) - Cannot find key of appropriate type to decrypt AP REP - AES256 CTS mode with HMAC SHA1-96)
keytab指定谬误导致
Failure unspecified at GSS-API level (Mechanism level: Request is a replay (34))
同步时钟无果
减少-Dsun.security.krb5.rcache=none
解决,参考(https://community.cloudera.com/t5/Support-Questions/Solr-quot-Request-is-a-replay-quot-Ambari-Infra-Solr-2-5/td-p/212870)
Failure unspecified at GSS-API level (Mechanism level: Checksum failed)
跟krb5.conf中配置的加密算法无关,测试下来上面两个办法选其一即可解决,参考(https://stackoverflow.com/questions/26784376/spnego-with-tomcat-error-gssexception-failure-unspecified-at-gss-api-level-me)
[realms] supported_enctypes = aes256-cts-hmac-sha1-96:special aes128-cts-hmac-sha1-96:special 或者 [libdefaults] default_tkt_enctypes = arcfour-hmac-md5
总结
本文基于spring的拦截器,实现了一个简化版的SPNEGO协定,能够帮忙加深kerberos认证流程和原理的了解,以及加深GSSAPI的原理意识。