上一篇Kafka安全机制解析及重构(一)中介绍了Kafka的安全认证的流程。其实Kafka官方也是推荐用户自己写一个安全认证模块的。官方在介绍SASL/PLAIN模式的时候是这样说的
- SASL/PLAIN should be used only with SSL as transport layer to ensure that clear passwords are not transmitted on the wire without encryption.
SASL/PLAIN模式应该搭配SSL协议使用,这样可以避免密码明文传输- The default implementation of SASL/PLAIN in Kafka specifies usernames and passwords in the JAAS configuration file as shown here. To avoid storing passwords on disk, you can plug in your own implementation of
javax.security.auth.spi.LoginModule
that provides usernames and passwords from an external source. The login module implementation should provide username as the public credential and password as the private credential of theSubject
. The default implementationorg.apache.kafka.common.security.plain.PlainLoginModule
can be used as an example.
由于SASL/PLAIN模式中密码是明文保存在JAAS配置文件中的,为了避免用户密码的明文保存,用户可以自己实现LoginModule接口来从别的位置获取用户密码。用户自己实现的login module需要将用户名和密码存放在Subject对象中,Kafka的PlainLoginModule可以作为一个示例供参考- In production systems, external authentication servers may implement password authentication. Kafka brokers can be integrated with these servers by adding your own implementation of
javax.security.sasl.SaslServer
. The default implementation included in Kafka in the packageorg.apache.kafka.common.security.plain
can be used as an example to get started.
在生产环境中,Kafka可以从外部的源获取用户密码信息,用户可以通过实现SaslServer接口来实现这个能力,plain模式的源码同样可以作为参考。
通过阅读PLAIN模式的源码,的确让我理清了JAAS的认证模式,自己实现的KAFKA的认证模块需要实现以下几个接口
- LoginModule,接口类是javax.security.auth.spi.LoginModule,这个类会在启动时被自动实例化。
- Provider,接口类是java.security.Provider,这个类一般在LoginModule实例化的时候被调起,将SaslClient和SaslServer的构造方法告知Sasl模块。一般在客户端写一个SaslClientProvider,把SaslClientFactory传入即可,而在Server端则需要写SaslClientProvider和SaslServerProvider,传入SaslClientFactory和SaslServerFactory。
- SaslClientFactory,接口类是javax.security.sasl.SaslClientFactory,在这个工厂类中构造出SaslClient。
- SaslServerFactory,接口类是javax.security.sasl.SaslServerFactory,在这个工厂类中构造出SaslServer。
- SaslClient,由工厂类SaslClientFactory生成,用于处理服务端请求,构造鉴权报文并发送。
- SaslServer,由工厂类SaslServerFactory生成,用于处理客户端请求,并进行用户密码认证。
这里我们同样把上一章中的流程图放出来,并增加一些更细节的说明,下面的源码分析可以就着这个图看。
我们先看看PLAIN模式的代码来热热身,源码我稍微缩减了一下,实际自己实现的时候最好别照搬,先实现出接口,然后看看有哪些方法是必须实现的。
public class PlainLoginModule implements LoginModule {
private static final String USERNAME_CONFIG = "username";
private static final String PASSWORD_CONFIG = "password";
static {
/**
/ 这里初始化PlainServerProvider,PLAIN模式没有ClientProvider,
/ 因为不需要进行客户端信息加密的工作,直接传明文。
**/
PlainSaslServerProvider.initialize();
}
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
// 从options里获取用户密码,option是从JAAS配置文件中读取的信息
String username = (String) options.get(USERNAME_CONFIG);
if (username != null)
subject.getPublicCredentials().add(username);
String password = (String) options.get(PASSWORD_CONFIG);
if (password != null)
subject.getPrivateCredentials().add(password);
}
}
PlainLoginModule这个类的作用是:
- 获取用户密码并将其放入Subject类中
- 初始化Provider类。
再来看Server的Provider类,这个类比较简单,是把安全机制及工厂类传入Security对象,供后续如果有需要就通过反射机制由该工厂类生成对应的PlainServer。
public class PlainSaslServerProvider extends Provider {
private static final long serialVersionUID = 1L;
protected PlainSaslServerProvider() {
super("Simple SASL/PLAIN Server Provider", 1.0, "Simple SASL/PLAIN Server Provider for Kafka");
super.put("SaslServerFactory." + PlainSaslServer.PLAIN_MECHANISM, PlainSaslServerFactory.class.getName());
}
public static void initialize() {
Security.addProvider(new PlainSaslServerProvider());
}
}
PlainSaslServerFactory类是用来构造PlainSaslServer的工厂类,也是比较简单的,PlainSaslServerFactory类在PlainSaslServer类中,实际如果自己要写的话最好分出一个新的类,不然构造的时候会出问题。SaslServerFactory接口必须实现两个方法:
- createSaslServer,首要是构造SaslServer,不然怎么叫工厂类呢
- getMechanismNames,上层需要知道这个工厂类对应的机制,比如PLAIN,SCRAM-SHA-256等等,如果自己写的话,像阿里的认证机制叫ONS,华为的叫DMS。
public static class PlainSaslServerFactory implements SaslServerFactory {
@Override
public SaslServer createSaslServer(String mechanism, String protocol, String serverName, Map props, CallbackHandler cbh)
throws SaslException {
if (!PLAIN_MECHANISM.equals(mechanism)) {
throw new SaslException(String.format("Mechanism \'%s\' is not supported. Only PLAIN is supported.", mechanism));
}
return new PlainSaslServer(cbh);
}
@Override
public String[] getMechanismNames(Map props) {
String noPlainText = (String) props.get(Sasl.POLICY_NOPLAINTEXT);
if ("true".equals(noPlainText))
return new String[]{};
else
return new String[]{PLAIN_MECHANISM};
}
}
重头戏是PlainSaslServer类,主要的方法就是evaluateResponse,这个方法被用于处理客户端发送过来的报文,在PLAIN模式中,报文被一个空字节切分成authorizationID,username,password三个元素。然后通过JaasUtils.defaultServerJaasConfigOption(JAAS_USER_PREFIX + username, PlainLoginModule.class.getName())方法来获取到JAAS配置文件中用户名对应的密码,比对通过则返回一个空字节报文至客户端。
public class PlainSaslServer implements SaslServer {
public static final String PLAIN_MECHANISM = "PLAIN";
private static final String JAAS_USER_PREFIX = "user_";
private boolean complete;
private String authorizationID;
public PlainSaslServer(CallbackHandler callbackHandler) {
}
@Override
public byte[] evaluateResponse(byte[] response) throws SaslException {
String[] tokens;
try {
tokens = new String(response, "UTF-8").split("\u0000");
} catch (UnsupportedEncodingException e) {
throw new SaslException("UTF-8 encoding not supported", e);
}
if (tokens.length != 3)
throw new SaslException("Invalid SASL/PLAIN response: expected 3 tokens, got " + tokens.length);
authorizationID = tokens[0];
String username = tokens[1];
String password = tokens[2];
if (username.isEmpty()) {
throw new SaslException("Authentication failed: username not specified");
}
if (password.isEmpty()) {
throw new SaslException("Authentication failed: password not specified");
}
if (authorizationID.isEmpty())
authorizationID = username;
try {
// 从配置文件中获取到expectedPassword,而后比对。
String expectedPassword = JaasUtils.defaultServerJaasConfigOption(JAAS_USER_PREFIX + username, PlainLoginModule.class.getName());
if (!password.equals(expectedPassword)) {
throw new SaslException("Authentication failed: Invalid username or password");
}
} catch (IOException e) {
throw new SaslException("Authentication failed: Invalid JAAS configuration", e);
}
complete = true;
return new byte[0];
}
}
我们可以发现PLAIN机制下的的SaslServer中并没有处理第3步new byte[0]的步骤,也没有第4步Server返回server token的步骤,而是直接进入了第5步发送用户权限信息,校验通过则返回空字节。这是因为在client里有个hasInitialResponse()方法,如果在自己实现的SaslClient中将这个方法的返回置为true,就直接进入3,4步。如果不跳过的话,SaslServer的evaluateResponse()方法就必须实现处理空字节客户端报文的逻辑(华为的客户端代码就这么干了)。我在自己重构的时候调试了半天,仔细读完代码才知道有这段逻辑在。
今天这篇介绍了一下kafka认证的关键类,解析了一下PLAIN模式,根据这些知识我们就可以实现一个自己的安全机制了,下一章中就介绍重构时候需要注意的几个要点吧。