关于java:使用Spring拦截器实现SPNEGO服务端

78次阅读

共计 5668 个字符,预计需要花费 15 分钟才能阅读完成。

在理清 SASL/GSSAPI/Kerberos 文中,咱们理解到,在 java 中能够通过 Krb5LoginModule 模块实现 Kerberos 登录,以及通过 GSSAPI 实现校验的根本流程;同时也明确 java 还定义了一个高层次接口 SASL 形象校验流程。校验流程并没有定义通过何种通信形式传递“票据”,用户能够本人开发适合的通信计划,而 SPNEGO 协定就是基于 http 传递票据和校验的规范计划,由微软提出,SPNEGO宽泛用于心愿集成 kerberos 认证的并且基于 http 的服务中。本文基于 spring interceptor 尝试实现一个繁难的 SPNEGO 服务。

筹备工作

咱们须要筹备一些测试所以须要的环境和物料。首先咱们先回顾一下基于 kerberos 认证拜访服务的整个过程:

1-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 的流程:

  1. 发送一般 http 申请
  2. 服务端返回 401 和 WWW-Authentication: Negotiation
  3. 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 编码字符串
  1. 服务端验证 xxxxxxx

代码实现

理解了基本原理后,着手开发,spring-security-kerberos 这个我的项目基于 spring security 框架实现了 kerberos 认证的服务。受这个我的项目启发,咱们用 interceptor 实现:

@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

以上是代码的根本逻辑,重点是看一下验证逻辑:

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 的实现如下:

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 的原理意识。

正文完
 0