初始化流程圖
ChannelBuilders.create創建ChannelBuilder對應關係如下:
switch (securityProtocol) {
case SSL:
requireNonNullMode(mode, securityProtocol);
channelBuilder = new SslChannelBuilder(mode);
break;
case SASL_SSL:
case SASL_PLAINTEXT:
requireNonNullMode(mode, securityProtocol);
if (loginType == null)
throw new IllegalArgumentException("`loginType` must be non-null if `securityProtocol` is `" + securityProtocol + "`");
if (mode == Mode.CLIENT && clientSaslMechanism == null)
throw new IllegalArgumentException("`clientSaslMechanism` must be non-null in client mode if `securityProtocol` is `" + securityProtocol + "`");
channelBuilder = new SaslChannelBuilder(mode, loginType, securityProtocol, clientSaslMechanism, saslHandshakeRequestEnable);
break;
case PLAINTEXT:
case TRACE:
channelBuilder = new PlaintextChannelBuilder();
break;
default:
throw new IllegalArgumentException("Unexpected securityProtocol " + securityProtocol);
}
這裏我們配置的listeners值是
listeners=SASL_PLAINTEXT://0.0.0.0:9092
故解析出來的protocol就是SASL_PLAINTEXT,相應的ChannelBuilder也就是SaslChannelBuilder
到這裏,權限驗證相關的組件算是構建完畢了,然後我們看當一個連接接進來的時候,這些組件是怎麼工作的。
入口是Accepter.accept
接下來是Processor線程
+ SaslChannelBuilder.buildChannel:
* 這裏會根據構造SaslChannelBuilder時傳進來的mode參數的不同選擇構造SaslServerAuthenticator還是SaslClientAuthenticator,我們這裏是服務端,當然是構造SaslServerAuthenticator.
* 構造好SaslServerAuthenticator後會調用它的configure函數,進行一些初始化配置。
* 把SaslServerAuthenticator對象作爲參數傳給KafkaChannel返回。
+ 這裏註冊好新的kafkaChannel後會調用poll函數完成一些IO的讀寫操作,而權限驗證的處理就以這裏爲入口。
+ pollSelectionKeys函數會處理所有可完成連接,可讀或可寫的KafkaChannel。而權限驗證部分則出現在KafkaChannel的準備階段。
+ authenticate這個函數內會根據目前握手所處的狀態的不同而做不同的處理,
* 首先是HANDSHAKE_REQUEST狀態,調用handleKafkaRequest處理第一階段的握手請求,解析出客戶端發來的mechanism,
根據mechanism創建SaslServer,代碼如下:
saslServer = Subject.doAs(subject, new PrivilegedExceptionAction<SaslServer>() {
public SaslServer run() throws SaslException {
return Sasl.createSaslServer(saslMechanism, "kafka", host, configs, callbackHandler);
}
});
這裏具體根據我們在jaas配置文件中的配置,最後是返回了一個PlainSaslServer,具體爲什麼返回了一個PlainSaslServer稍後講。
* 然後下一狀態是AUTHENTICATE,驗證客戶端發來的明文用戶名和密碼,調用了PlainSaslServer的evaluateResponse,代碼如下
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 {
String expectedPassword = JaasUtils.jaasConfig(LoginType.SERVER.contextName(), JAAS_USER_PREFIX + username);
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);
}
所以這裏可以根據我們的需求根據客戶端傳過來的username和password動態的去某一個數據源獲取和匹配其合法性。
jaas配置文件:
KafkaServer {
org.apache.kafka.common.security.plain.PlainLoginModule required
username="xxxxx"
password="yyyyy"
user_xxxxx="yyyyy";
};
看到這裏我們配置了一個org.apache.kafka.common.security.plain.PlainLoginModule
上面講到返回了一個PlainSaslServer,具體是怎麼返回的呢?我們需要從SaslChannelBuilder的configure說起,上流程圖:
+ LoginContext.init這裏會初始化ConfigFile,讀取jaas配置文件中的內容
+ LoginContext.login這裏會實例化jaas配置文件中的PlainLoginModule,並依次調用其initialize和login函數,
其中initialize函數會把username和password配置到subject裏面去,這個subject最終會通過LoginManager的subject函數在SaslChannelBuilder的buildChannel函數中獲取到,設置到SaslClientAuthenticator中用於和其他服務器通訊驗證使用。
+ 而PlainLoginModule可不僅僅只做了這一件事情,該類定義了一個靜態塊初始化代碼,調用了PlainSaslServerProvider的initialize函數用於註冊創建負責做PLAIN協議驗證的類的工廠類PlainSaslServerFactory。代碼如下:
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());
}
我們看到註冊的是一個SaslServerFactory.PLAIN -> org.apache.kafka.common.security.plain.PlainSaslServer.PlainSaslServerFactory的對應關係
而再看前面調用的Sasl的createSaslServer的代碼:
String mechFilter = "SaslServerFactory." + mechanism;
Provider[] provs = Security.getProviders(mechFilter);
for (int j = 0; provs != null && j < provs.length; j++) {
className = provs[j].getProperty(mechFilter);
if (className == null) {
throw new SaslException("Provider does not support " +
mechFilter);
}
fac = (SaslServerFactory) loadFactory(provs[j], className);
if (fac != null) {
mech = fac.createSaslServer(
mechanism, protocol, serverName, props, cbh);
if (mech != null) {
return mech;
}
}
}
其中mechFilter的值即是SaslServerFactory.PLAIN,這裏取到PlainSaslServerFactory然後調用createSaslServer方法返回了一個PlainSaslServer
綜上,kafka內部的整個權限驗證的初始化流程和驗證邏輯已經比較清晰了(可能講的比較亂,反正我是清晰了),但是我們發現,跟網上的其他自己編寫LoginModule模塊做驗證的方式不同,kafka的PlainLoginModule這個類本身並沒有做什麼跟驗證有關的邏輯,
只是做了一些初始化和註冊provider的工作,而真正做權限驗證的是從provider間接生產出來的PlainSaslServer類。