1. 概述
在本教程中,我们将介绍简单身份验证和安全层(SASL)的基础知识,我们将了解Java如何支持采用SASL来保护通信。
在此过程中,我们将使用简单的客户端和服务器通信,并使用SASL对其进行保护。
2. 什么是SASL?
SASL是互联网协议中用于身份验证和数据安全的框架,它旨在将互联网协议与特定的身份验证机制分离。随着我们的深入,我们将更好地理解这个定义的部分内容。
通信中对安全性的需求是隐含的,让我们尝试在客户端和服务器通信的上下文中理解这一点。通常,客户端和服务器通过网络交换数据,双方必须相互信任并安全地发送数据。
2.1 SASL适用于哪些领域?
在应用程序中,我们可能会使用SMTP发送电子邮件并使用LDAP访问目录服务。但是这些协议中的每一个都可能支持另一种身份验证机制,例如Digest-MD5或Kerberos。
如果有一种方法可以让协议更明确地交换身份验证机制呢?这正是SASL发挥作用的地方,支持SASL的协议可以始终支持任何SASL机制。
因此,应用程序可以协商合适的机制并采用该机制进行身份验证和安全通信。
2.2 SASL是如何工作的?
现在,我们已经了解了SASL在整体安全方案中的位置,让我们了解它是如何工作的。
SASL是一个质询-响应框架。在这里,服务器向客户端发出质询,客户端根据质询发送响应。质询和响应是任意长度的字节数组,因此可以携带任何特定于机制的数据。
这种交换可以持续多次迭代,并最终在服务器不再发出进一步质询时结束。
此外,客户端和服务器可以协商安全层后验证,所有后续通信都可以利用此安全层。但是,请注意,某些机制可能仅支持身份验证。
重要的是要了解SASL仅提供用于交换质询和响应数据的框架,它没有提及有关数据本身或它们如何交换的任何信息,这些细节留给采用SASL的应用程序。
3. Java中的SASL支持
Java中有一些API支持使用SASL开发客户端和服务器端应用程序,API不依赖于实际机制本身,使用Java SASL API的应用程序可以根据所需的安全特性选择一种机制。
3.1 Java SASL API
作为包“javax.security.sasl”的一部分,需要注意的关键接口是SaslServer和SaslClient。
SaslServer代表SASL的服务器端机制。
让我们看看如何实例化一个SaslServer:
SaslServer ss = Sasl.createSaslServer(
mechanism,
protocol,
serverName,
props,
callbackHandler);
我们使用工厂类Sasl来实例化SaslServer,createSaslServer方法接收几个参数:
- mechanism:SASL支持机制的IANA注册名称
- protocol:正在执行身份验证的协议的名称
- serverName:服务器的完全限定主机名
- props:用于配置身份验证交换的一组属性
- callbackHandler:所选机制使用的回调处理程序以获取更多信息
在上面,只有前两个是强制的,其余的都是可以为空的。
SaslClient表示SASL的客户端机制,让我们看看如何实例化一个SaslClient:
SaslClient sc = Sasl.createSaslClient(
mechanisms,
authorizationId,
protocol,
serverName,
props,
callbackHandler);
在这里,我们再次使用工厂类Sasl来实例化我们的SaslClient。createSaslClient接收的参数列表与以前几乎相同。
但是,存在一些细微差别:
- mechanisms:在这里,这是一个可以尝试的机制列表
- authorizationId:这是用于授权的依赖于协议的标识
其余参数的含义和可选性相似。
3.2 Java SASL安全提供程序
在Java SASL API之下是提供安全功能的实际机制,这些机制的实现由在Java加密体系结构(JCA)中注册的安全提供程序提供。
可以有多个向JCA注册的安全提供程序,其中每一个都可以支持一个或多个SASL机制。
Java附带SunSASL作为安全提供程序,它默认注册为JCA提供程序。但是,这可能会被删除或与任何其他可用的提供程序重新排序。
此外,始终可以提供自定义安全提供程序,这将需要我们实现接口SaslClient和SaslServer。这样做,我们也可以实现我们的自定义安全机制!
4. SASL实例
现在我们已经了解了如何创建SaslServer和SaslClient,是时候了解如何使用它们了。我们将开发客户端和服务器组件,这些将迭代地交换质询和响应以实现身份验证。我们将在此处的简单示例中使用DIGEST-MD5机制。
4.1 客户端和服务器CallbackHandler
正如我们之前看到的,我们需要为SaslServer和SaslClient提供CallbackHandler的实现。现在,CallbackHandler是一个简单的接口,它定义了单个方法-handle,此方法接收Callback数组。
在这里,Callback为安全机制提供了一种从调用应用程序收集身份验证数据的方法。例如,安全机制可能需要用户名和密码。有相当多的Callback实现可供使用,例如NameCallback和PasswordCallback。
让我们看看如何为服务器定义一个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;
// Perform application-specific authorization action
ac.setAuthorized(true);
} else if (cb instanceof NameCallback) {
NameCallback nc = (NameCallback) cb;
// Collect username in application-specific manner
nc.setName("username");
} else if (cb instanceof PasswordCallback) {
PasswordCallback pc = (PasswordCallback) cb;
// Collect password in application-specific manner
pc.setPassword("password".toCharArray());
} else if (cb instanceof RealmCallback) {
RealmCallback rc = (RealmCallback) cb;
// Collect realm data in application-specific manner
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;
// Collect username in application-specific manner
nc.setName("username");
} else if (cb instanceof PasswordCallback) {
PasswordCallback pc = (PasswordCallback) cb;
// Collect password in application-specific manner
pc.setPassword("password".toCharArray());
} else if (cb instanceof RealmCallback) {
RealmCallback rc = (RealmCallback) cb;
// Collect realm data in application-specific manner
rc.setText("myServer");
}
}
}
}
为澄清起见,我们循环遍历Callback数组并仅处理特定的回调。我们必须处理的是特定于使用的机制,这里是DIGEST-MD5。
4.2 SASL身份验证
因此,我们已经编写了客户端和服务器CallbackHandler。我们还为DIGEST-MD5机制实例化了SaslClient和SaslServer。
现在是时候看看它们的实际应用了:
@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());
}
让我们试着了解这里发生了什么:
- 首先,我们的客户端从服务器获取默认质询
- 然后客户评估质询并准备响应
- 这种质询-响应交换持续了一个周期
- 在此过程中,客户端和服务器使用回调处理程序来收集该机制所需的任何其他数据
- 我们的身份验证到此结束,但实际上,它可以迭代多个周期
质询和响应字节数组的典型交换发生在网络上。但是,这里为了简单起见,我们假设本地通信。
4.3 SASL安全通信
正如我们之前讨论的那样,SASL是一个能够支持安全通信而不仅仅是身份验证的框架。然而,这只有在底层机制支持的情况下才有可能。
首先,让我们先检查一下我们是否能够协商安全通信:
String qop = (String) saslClient.getNegotiatedProperty(Sasl.QOP);
assertEquals("auth-conf", qop);
这里,QOP代表保护质量。这是客户端和服务器在身份验证期间协商的内容。“auth-int”值表示身份验证和完整性,而“auth-conf”的值表示身份验证、完整性和机密性。
一旦我们有了安全层,我们就可以利用它来保护我们的通信。
让我们看看如何保护客户端中的传出通信:
byte[] outgoing = "Tuyucheng".getBytes();
byte[] secureOutgoing = saslClient.wrap(outgoing, 0, outgoing.length);
// Send secureOutgoing to the server over the network
同样,服务器可以处理传入的通信:
// Receive secureIncoming from the client over the network
byte[] incoming = saslServer.unwrap(secureIncoming, 0, netIn.length);
assertEquals("Tuyucheng", new String(incoming, StandardCharsets.UTF_8));
5. 现实世界中的SASL
因此,我们现在对什么是SASL以及如何在Java中使用它有了一个清晰的了解。但是,通常情况下,这不是我们最终使用SASL的目的,至少在我们的日常工作中是这样。
正如我们之前看到的,SASL主要用于LDAP和SMTP等协议。尽管如此,越来越多的应用程序开始使用SASL-例如Kafka。那么,我们如何使用SASL对此类服务进行身份验证呢?
假设我们已经为SASL配置了Kafka Broker,并将PLAIN作为选择的机制。PLAIN只是意味着它使用明文形式的用户名和密码组合进行身份验证。
现在让我们看看如何配置Java客户端以使用SASL/PLAIN对Kafka 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安全套接字扩展(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相比,这里的选择有限且重量级。
总的来说,SASL是一个非常轻量级的框架,并通过可插拔机制提供了广泛的安全特性。采用SASL的应用程序在实现正确的安全功能集方面有很多选择,具体取决于需要。
7. 总结
综上所述,在本教程中,我们了解了提供身份验证和安全通信的SASL框架的基础知识。我们还讨论了Java中可用于实现SASL客户端和服务器端的API。
我们看到了如何通过JCA提供程序使用安全机制。最后,我们还讨论了SASL在处理不同协议和应用程序时的用法。
与往常一样,本教程的完整源代码可在GitHub上获得。