Java SASL 入门

1、概览

本文将带你了解 Simple Authentication and Security Layer简单身份验证和安全层,SASL)的基础知识,以及如何在 Java 中使用 SASL 确保通信安全。

主要是使用 SASL 来确保客户端和服务端通信的安全。

2、SASL 是什么?

SASL 是互联网协议中的身份验证和数据安全框架。它旨在将互联网协议与特定的身份验证机制分离开来。

通信安全的需求是不言而喻的。从客户端和服务器通信的角度可以更好的理解这一点。客户端和服务器通常通过网络交换数据。双方必须相互信任并安全地发送数据。

2.1、SASL 的定位是什么?

在应用中,我们可能使用 SMTP 发送电子邮件,使用 LDAP 访问目录服务。但每种协议都可能支持另一种身份验证机制,如 Digest-MD5Kerberos

如果有一种方法可以让协议以声明的方式交换身份验证机制,那会怎么样呢?这正是 SASL 的用武之地。支持 SASL 的协议总是可以支持任何一种 SASL 机制。

因此,应用可以协商一个合适的机制,并采用该机制进行身份验证和安全通信。

2.2、SASL 的原理是什么?

了解了 SASL 在整个 安全通信 中的位置后,让我们来了解一下它是如何工作的。

SASL 是一种 challenge-response 框架。在这里,服务器向客户端发出 challenge,客户端根据 challenge 发送响应。challenge 和响应是任意长度的字节数组,因此可以携带任何特定机制的数据。

SASL Exchange

这种交换会持续多次,最后在服务器不再发出 exchange 时结束。

此外,客户端和服务器还可以在验证后协商一个安全层(Security Layer)。所有后续通信都可以利用这个安全层。不过,需要注意的是,有些机制可能只支持身份验证。

重要的是要明白,SASL 只提供了一个交换 exchange 和响应数据的框架。它没有提及任何有关数据本身或如何交换数据的内容。这些细节留给采用 SASL 的应用去处理。

3、Java 中的 SASL 支持

Java 中有一些 API,可支持使用 SASL 开发客户端和服务器端应用。API 并不依赖于实际机制本身。使用 Java SASL API 的应用可根据所需的安全功能选择一种机制。

3.1、Java SASL API

需要注意的关键接口是 SaslServerSaslClient,它们都在 javax.security.sasl 包下。

SaslServer 表示 SASL 的服务器端机制。

实例化 SaslServer

SaslServer ss = Sasl.createSaslServer(
  mechanism, 
  protocol, 
  serverName, 
  props, 
  callbackHandler);

使用工厂类 Sasl 来实例化 SaslServercreateSaslServer 方法接受几个参数:

  • mechanism - 支持 SASL 的机制的 IANA 注册名称
  • protocol - 进行身份验证的协议名称
  • serverName - 服务器的完整主机名
  • props - 用于配置身份验证交换的一组属性
  • callbackHandler - 回调处理器,供所选机制用于获取更多信息

在上述参数中,只有前两项是必须的,其余都是可选的。

SaslClient 代表 SASL 的客户端机制。

何实例化 SaslClient

SaslClient sc = Sasl.createSaslClient(
  mechanisms, 
  authorizationId, 
  protocol, 
  serverName, 
  props,
  callbackHandler);

同样,再次使用工厂类 Sasl 来实例化 SaslClientcreateSaslClient 接受的参数列表与之前的基本相同。

不过,两者之间也有一些细微的差别:

  • mechanisms - 一个机制列表,可从这些机制中进行尝试
  • authorizationId - 用于授权的协议标识

其余参数的含义和可选性都类似。

3.2、Java SASL Security Provider

Java SASL API 下面是提供安全功能的实际机制。这些机制的实现由在 Java Cryptography Architecture(JCA)注册的 Security Provider 提供。

可以有多个 Security Provider 在 JCA 注册。每个 Security Provider 都可能支持一种或多种 SASL 机制。

Java 使用 SunSASL 作为 Security Provider,默认注册为 JCA Security Provider。不过,它可以被移除或与任何其他可用 Security Provider 一起重新排序。

此外,我们还可以提供自定义的 Security Provider。这就要求我们实现 SaslClientSaslServer 接口。这样,我们也可以实现自定义的安全机制!

4、SASL 示例

了解了如何创建 SaslServerSaslClient 后,我们来开发客户端和服务器组件。它们将反复交换 challenge 和响应,以实现身份验证。在这个简单的示例中,我们使用 DIGEST-MD5 机制。

4.1、客户端和服务器 CallbackHandler

如前所述,我们需要为 SaslServerSaslClient 提供 CallbackHandler 的实现。

CallbackHandler 是一个简单的接口,只定义了一个方法 handle。该方法接受一个 Callback 数组。

Callback 为安全机制提供了一种从调用方收集身份验证数据的方法。例如,安全机制可能需要用户名和密码。有许多 Callback 实现(如 NameCallbackPasswordCallback)可供使用。

先看看如何为服务器定义 CallbackHandler

public class ServerCallbackHandler implements CallbackHandler {
    @Override
    public void handle(Callback[] cbs) throws IOException, UnsupportedCallbackException {
        for (Callback cb : cbs) {
            if (cb instanceof AuthorizeCallback) {
                AuthorizeCallback ac = (AuthorizeCallback) cb;
                //执行针对特定应用程序的授权操作
                ac.setAuthorized(true);
            } else if (cb instanceof NameCallback) {
                NameCallback nc = (NameCallback) cb;
                //以特定应用程序的方式收集用户名
                nc.setName("username");
            } else if (cb instanceof PasswordCallback) {
                PasswordCallback pc = (PasswordCallback) cb;
                //以特定应用程序的方式收集密码
                pc.setPassword("password".toCharArray());
            } else if (cb instanceof RealmCallback) { 
                RealmCallback rc = (RealmCallback) cb; 
                //以特定应用方式收集 realm  数据 
                rc.setText("myServer"); 
            }
        }
    }
}

现在,来看看客户端的 Callbackhandler

public class ClientCallbackHandler implements CallbackHandler {
    @Override
    public void handle(Callback[] cbs) throws IOException, UnsupportedCallbackException {
        for (Callback cb : cbs) {
            if (cb instanceof NameCallback) {
                NameCallback nc = (NameCallback) cb;
                //以特定应用程序的方式收集用户名
                nc.setName("username");
            } else if (cb instanceof PasswordCallback) {
                PasswordCallback pc = (PasswordCallback) cb;
                //以特定应用程序的方式收集密码
                pc.setPassword("password".toCharArray());
            } else if (cb instanceof RealmCallback) { 
                RealmCallback rc = (RealmCallback) cb; 
                //以特定应用方式收集 realm 数据 
                rc.setText("myServer"); 
            }
        }
    }
}

说明一下,我们迭代 Callback 数组,只处理特定的 Callback。我们要处理的是所使用机制的特定 Callback,这里是 DIGEST-MD5

4.2、SASL 认证

至此,我们已经编写了客户端和服务器 CallbackHandler。我们还为 DIGEST-MD5 机制实例化了 SaslClientSaslServer

现在来看看实际应用:

@Test
public void givenHandlers_whenStarted_thenAutenticationWorks() throws SaslException {
    byte[] challenge;
    byte[] response;
 
    challenge = saslServer.evaluateResponse(new byte[0]);
    response = saslClient.evaluateChallenge(challenge);
 
    challenge = saslServer.evaluateResponse(response);
    response = saslClient.evaluateChallenge(challenge);
 
    assertTrue(saslServer.isComplete());
    assertTrue(saslClient.isComplete());
}

如上:

  • 首先,客户端从服务器获取默认 challenge
  • 然后,客户对 challenge 进行评估并准备响应
  • 这种 “challenge - 响应” 的通信还要持续一个周期
  • 在此过程中,客户端和服务器利用 Callback Handler 收集机制所需的任何其他数据
  • 验证到此结束,但实际上,它可以重复多个循环

exchange 和响应字节数组的典型交换是通过网络进行的。但是,为了简单起见,我们在这里假定是本地通信。

4.3、SASL 安全通信

如前所述,SASL 是一个能够支持安全通信的框架,而不仅仅是身份验证。然而,这只有在底层机制支持的情况下才有可能。

首先,检查一下我们是否能够协商出一个安全的通信:

String qop = (String) saslClient.getNegotiatedProperty(Sasl.QOP);
 
assertEquals("auth-conf", qop);

QOP 代表保护质量。这是客户端和服务器在身份验证过程中协商确定的。auth-int 表示身份验证和完整性。而 auth-conf 的值则表示身份验证、完整性和保密性。

一旦有了 Security Layer,我们就可以利用它来确保通信安全。

来看看如何确保客户端与外部发通信的安全:

byte[] outgoing = "Baeldung".getBytes();
byte[] secureOutgoing = saslClient.wrap(outgoing, 0, outgoing.length);
 
// 通过网络向服务器发送 secureOutgoing 信息

同样,服务器也可以处理传入的通信:

// 通过网络接收来自客户端的 secureIncoming 信息
byte[] incoming = saslServer.unwrap(secureIncoming, 0, netIn.length);
 
assertEquals("Baeldung", new String(incoming, StandardCharsets.UTF_8));

5、SASL 在现实世界中的应用

现在,我们对什么是 SASL 以及如何在 Java 中使用 SASL 有了一定的了解。但通常情况下,这并不是我们最终使用 SASL 的目的,至少在我们的日常工作中不是这样。

如前所述,SASL 主要用于 LDAPSMTP 等协议。不过,越来越多的应用开始使用 SASL,例如 Kafka。那么,我们该如何使用 SASL 对此类服务进行身份验证呢?

假设我们为 Kafka Broker 配置了 SASL,并将 PLAIN 作为首选机制。PLAIN 简单地说就是使用纯文本的用户名和密码组合进行身份验证。

现在让我们来看看如何配置 Java 客户端使用 SASL/PLAINKafka Broker 进行身份验证。

首先提供一个简单的 JAAS 配置,即 kafka_jaas.conf

KafkaClient {
  org.apache.kafka.common.security.plain.PlainLoginModule required
  username="username"
  password="password";
};

在启动 JVM 时使用 JAAS 配置:

-Djava.security.auth.login.config=kafka_jaas.conf

最后,必须添加一些属性,传递给生产者和消费者实例:

security.protocol=SASL_SSL
sasl.mechanism=PLAIN

仅此配置就行。不过,这只是 Kafka 客户端配置的一小部分。除了 PLAIN,Kafka 还支持 GSSAPI/Kerberos 身份验证。

6、SASL 对比

虽然 SASL 在提供一种机制中立的客户端和服务器通信验证和安全方式方面相当有效。但是,SASL 并不是这方面唯一可用的解决方案。

Java 本身提供了其他机制来实现这一目标。我们将简要讨论这些机制,并了解它们与 SASL 的对比情况:

  • Java Secure Socket Extension(JSSE): JSSE 是 Java 中为 Java 实现安全套接字层(SSL)的一组软件包。它提供数据加密、客户端和服务器验证以及消息完整性。与 SASL 不同,JSSE 依靠公钥基础设施(PKI)来工作。因此,SASL 比 JSSE 更灵活、更轻便。
  • Java GSS API (JGSS): JGGS 是通用安全服务应用编程接口 (GSS-API) 的 Java 语言绑定。GSS-API 是 IETF 标准,用于应用程序访问安全服务。在 Java 中,GSS-API 支持的唯一机制是 Kerberos。Kerberos 同样需要 Kerberised 基础设施才能工作。与 SASL 相比,Kerberos 的选择是有限的,而且是重量级的。

总的来说,SASL 是一个非常轻量级的框架,可通过可插拔机制提供多种安全功能。采用 SASL 的应用程序有很多选择,可以根据需要实施合适的安全功能。

7、总结

本文介绍了 SASL 框架的基础知识,以及如何在 Java 中使用 SASL 客户端和服务器端 API 来提供身份验证和安全通信。


Ref:https://www.baeldung.com/java-sasl