Zookeeper 安全
本章将对 Zookeeper 项目中安全相关内容进行分析。本章主要围绕AuthenticationProvider接口进行相关分析。
AuthenticationProvider 分析
本节将对AuthenticationProvider接口进行简单概述,首先需要了解该接口的定义,完整定义代码如下。
public interface AuthenticationProvider {
/**
* 获取验证方案
*/
String getScheme();
/**
* 处理认证
*/
KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte authData[]);
/**
* id和aclExpr是否匹配
*/
boolean matches(String id, String aclExpr);
/**
* 是否通过身份验证
*/
boolean isAuthenticated();
/**
* 验证id语法
*/
boolean isValid(String id);
}
在AuthenticationProvider接口中提供了五个方法分别是。
- 方法getScheme用于获取验证方案。
- 方法handleAuthentication用于对安全数据进行认证。
- 方法matches用于判断id和acl数据信息是否匹配。
- 方法isAuthenticated用于判断是否通过身份验证。
- 方法isValid用于验证id语法。
在Zookeeper项目中AuthenticationProvider接口存在多个实现类,具体实现类如图所示。
X509AuthenticationProvider 分析
本节将对X509AuthenticationProvider类进行分析,从该类的命名中可不难发现它的安全认证策略是x509。接下来对AuthenticationProvider接口中需要实现的方法进行分析,首先是isValid方法的分析,具体处理代码如下。
@Override
public boolean isValid(String id) {
try {
new X500Principal(id);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
在上述代码中会将参数id放入到X500Principal类的构造函数中进行创建,如果创建出现了异常则表示id语法不正确,反之则表示语法正确。接下来对matches方法进行分析,具体处理代码如下。
@Override
public boolean matches(String id, String aclExpr) {
if (System.getProperty(ZOOKEEPER_X509AUTHENTICATIONPROVIDER_SUPERUSER) != null) {
return (id.equals(System.getProperty(ZOOKEEPER_X509AUTHENTICATIONPROVIDER_SUPERUSER))
|| id.equals(aclExpr));
}
return (id.equals(aclExpr));
}
在上述代码中处理流程如下。
- 判断系统变量中zookeeper.X509AuthenticationProvider.superUser属性是否存在,如果不存在则将id和aclExpr进行字符串相等比较并将其返回。如果存在则进行如下操作。
- id和zookeeper.X509AuthenticationProvider.superUser属性进行字符串相等比较。
- id和aclExpr进行字符串相等比较。
- 上述两个比较结果进行或运算返回。
最后对handleAuthentication方法进行分析,具体处理代码如下。
@Override
public KeeperException.Code handleAuthentication(ServerCnxn cnxn,
byte[] authData) {
// 从连接对象中获取证书集合
X509Certificate[] certChain
= (X509Certificate[]) cnxn.getClientCertificateChain();
// 证书集合为空或者证书集合长度为0返回认证失败状态码
if (certChain == null || certChain.length == 0) {
return KeeperException.Code.AUTHFAILED;
}
// 信任管理器为空返回认证失败状态码
if (trustManager == null) {
LOG.error("No trust manager available to authenticate session 0x{}",
Long.toHexString(cnxn.getSessionId()));
return KeeperException.Code.AUTHFAILED;
}
// 取出证书集合中的第一个证书
X509Certificate clientCert = certChain[0];
// 安全认证,认证中出现CertificateException异常返回认证失败状态码
try {
trustManager.checkClientTrusted(certChain,
clientCert.getPublicKey().getAlgorithm());
} catch (CertificateException ce) {
LOG.error("Failed to trust certificate for session 0x" +
Long.toHexString(cnxn.getSessionId()), ce);
return KeeperException.Code.AUTHFAILED;
}
// 根据客户端证书获取证书id
String clientId = getClientId(clientCert);
// 判断证书id和zookeeper.X509AuthenticationProvider.superUser属性是否相同,相同则创建id对象加入到连接对象中
if (clientId.equals(System.getProperty(
ZOOKEEPER_X509AUTHENTICATIONPROVIDER_SUPERUSER))) {
cnxn.addAuthInfo(new Id("super", clientId));
LOG.info("Authenticated Id '{}' as super user", clientId);
}
// 创建id对象加入到连接对象中
Id authInfo = new Id(getScheme(), clientId);
cnxn.addAuthInfo(authInfo);
LOG.info("Authenticated Id '{}' for Scheme '{}'",
authInfo.getId(), authInfo.getScheme());
// 返回认证通过
return KeeperException.Code.OK;
}
在上述代码中核心处理流程如下。
- 从连接对象中获取证书集合,如果证书集合为空或者证书集合长度为0,则返回认证失败状态码。
- 如果成员变量trustManager(信任管理器)为空,则返回认证失败状态码。
- 从证书集合中获取第一个证书。
- 进行安全认证(处理方法trustManager.checkClientTrusted),如果在安全认证中出现CertificateException异常返回认证失败状态码。
- 从客户端证书中获取证书id,判断证书id和zookeeper.X509AuthenticationProvider.superUser属性是否相同,相同则创建id对象加入到连接对象中,此时加入的Id对象方案是super,表示超级管理员。
- 创建Id对象,将其放入到连接对象中,返回认证通过。
DigestAuthenticationProvider 分析
本节将对DigestAuthenticationProvider类进行分析,从该类的命名中可不难发现它的安全认证策略是digest。接下来对AuthenticationProvider接口中需要实现的方法进行分析,首先是isValid方法的分析,具体处理代码如下。
public boolean isValid(String id) {
String parts[] = id.split(":");
return parts.length == 2;
}
在上述代码中对于id语法合理性的判断是根据冒号切分后长度是否是2。接下来对matches方法进行分析,具体处理代码如下。
public boolean matches(String id, String aclExpr) {
return id.equals(aclExpr);
}
在上述代码中将判断id和aclExpr是否相同。最后对handleAuthentication方法进行分析,具体处理代码如下。
public KeeperException.Code
handleAuthentication(ServerCnxn cnxn, byte[] authData) {
// 将认证信息转换为字符串
String id = new String(authData);
try {
// 生成摘要
String digest = generateDigest(id);
// 摘要和超级管理员摘要对比,如果相同创建id对象加入到连接对象中
if (digest.equals(superDigest)) {
cnxn.addAuthInfo(new Id("super", ""));
}
// 创建id对象加入到连接对象中
cnxn.addAuthInfo(new Id(getScheme(), digest));
return KeeperException.Code.OK;
} catch (NoSuchAlgorithmException e) {
LOG.error("Missing algorithm", e);
}
return KeeperException.Code.AUTHFAILED;
}
在上述代码中核心处理流程如下。
- 将认证信息转换为字符串。
- 将第(1)步中的字符串进行摘要算法计算。
- 若第(2)步的摘要算法结果和超级管理员摘要(超级管理员摘要存储在系统变量zookeeper.DigestAuthenticationProvider.superDigest中)对比,如果相同则创建id对象加入到连接对象中,此时加入的Id对象方案是super,表示超级管理员。
- 创建Id对象,将其放入到连接对象中,返回认证通过。
- 如果在第(2)~(4)步中出现异常将进行异常日志记录,并返回认证失败。
IPAuthenticationProvider 分析
本节将对IPAuthenticationProvider类进行分析,从该类的命名中可不难发现它的安全认证策略是ip。接下来对AuthenticationProvider接口中需要实现的方法进行分析,首先是isValid方法的分析,具体处理代码如下。
public boolean isValid(String id) {
// 按照斜杠拆分
String parts[] = id.split("/", 2);
// 将拆分结果的第一个元素转换为字节,第一个元素是ip
byte aclAddr[] = addr2Bytes(parts[0]);
// ip字节为空返回false
if (aclAddr == null) {
return false;
}
// 拆分结果长度为2
if (parts.length == 2) {
try {
// 将第二个元素转换为int,第二个元素是bits
int bits = Integer.parseInt(parts[1]);
// bits小于0或者 aclAddr.length * 8值
if (bits < 0 || bits > aclAddr.length * 8) {
return false;
}
} catch (NumberFormatException e) {
return false;
}
}
return true;
}
在上述代码中首先需要明确id的定义,Zookeeper项目中对于这个id的定义:ip/bits。上述代码处理流程如下。
- 将参数id按照斜杠进行拆分。
- 将拆分结果的第一个元素转换为字节数组,如果字节数组为空将返回false。
- 如果拆分结果长度为2则需要对bits进行判断,若bits小于0或者bits的数值大于ip字节长度的八倍将返回false,若bits解析为Integer时出现异常也将返回false。
- 返回true表示id语义合法。
接下来对matches方法进行分析,具体处理代码如下。
public boolean matches(String id, String aclExpr) {
String parts[] = aclExpr.split("/", 2);
byte aclAddr[] = addr2Bytes(parts[0]);
if (aclAddr == null) {
return false;
}
int bits = aclAddr.length * 8;
if (parts.length == 2) {
try {
bits = Integer.parseInt(parts[1]);
if (bits < 0 || bits > aclAddr.length * 8) {
return false;
}
} catch (NumberFormatException e) {
return false;
}
}
// 对字节数组进行加工
mask(aclAddr, bits);
// 将id转换为字节数组
byte remoteAddr[] = addr2Bytes(id);
if (remoteAddr == null) {
return false;
}
// 对字节数组进行加工
mask(remoteAddr, bits);
// 遍历字节判断是否和acl中的数据相同,如果不相同则返回false
for (int i = 0; i < remoteAddr.length; i++) {
if (remoteAddr[i] != aclAddr[i]) {
return false;
}
}
return true;
}
在上述代码中核心操作是对id和aclExpr先进行字节数组转换然后通过mask方法配合得到包装后的字节数组,然后对比两者的数据,如果出现不相同则返回false表示不匹配。
最后对handleAuthentication方法进行分析,具体处理代码如下。
public KeeperException.Code
handleAuthentication(ServerCnxn cnxn, byte[] authData) {
String id = cnxn.getRemoteSocketAddress().getAddress().getHostAddress();
cnxn.addAuthInfo(new Id(getScheme(), id));
return KeeperException.Code.OK;
}
在上述代码中对处理流程如下。
- 从连接对象中获取host将其命名为id。
- 创建Id对象放入到连接对象中。
- 返回认证通过。
SASLAuthenticationProvider 分析
本节将对DigestAuthenticationProvider类进行分析,从该类的命名中可不难发现它的安全认证策略是sasl。
接下来对AuthenticationProvider接口中需要实现的方法进行分析,首先是isValid方法的分析,具体处理代码如下。
public boolean isValid(String id) {
try {
new KerberosName(id);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
在上述代码中会将参数id放入到KerberosName类的构造函数中进行创建,如果创建出现了异常则表示id语法不正确,反之则表示语法正确。接下来对matches方法进行分析,具体处理代码如下。
public boolean matches(String id, String aclExpr) {
if ((id.equals("super") || id.equals(aclExpr))) {
return true;
}
String readAccessUser = System.getProperty("zookeeper.letAnySaslUserDoX");
if (readAccessUser != null && aclExpr.equals(readAccessUser)) {
return true;
}
return false;
}
在上述代码中处理流程如下。
- 满足下面两个条件中的任意一个返回true。
- id和super相同。
- id和aclExpr相同
- 从环境变量中取出zookeeper.letAnySaslUserDoX属性,判断该属性和aclExpr是否相同如果是则返回true。
- 在不满足上述两个条件的情况下返回false。
ProviderRegistry 分析
在Zookeeper项目中如果需要使用安全认证相关的实现一般会涉及到ProviderRegistry类,通常通过ProviderRegistry类获取具体的安全认证实现类(接口AuthenticationProvider的实现类),本节将对ProviderRegistry类进行分析,在该类中最重要的方法是initialize,具体处理代码如下。
public static void initialize() {
synchronized (ProviderRegistry.class) {
// 如果已经实例化则跳过
if (initialized) {
return;
}
// 创建 ip 和 digest 的认证模式加入到容器中
IPAuthenticationProvider ipp = new IPAuthenticationProvider();
DigestAuthenticationProvider digp = new DigestAuthenticationProvider();
authenticationProviders.put(ipp.getScheme(), ipp);
authenticationProviders.put(digp.getScheme(), digp);
// 获取系统变量
Enumeration<Object> en = System.getProperties().keys();
while (en.hasMoreElements()) {
String k = (String) en.nextElement();
// 如果系统变量是以zookeeper.authProvider.开头
if (k.startsWith(AUTHPROVIDER_PROPERTY_PREFIX)) {
// 获取属性值
String className = System.getProperty(k);
try {
// 反射创建类对象
Class<?> c = ZooKeeperServer.class.getClassLoader()
.loadClass(className);
AuthenticationProvider ap =
(AuthenticationProvider) c.getDeclaredConstructor()
.newInstance();
// 加入到容器
authenticationProviders.put(ap.getScheme(), ap);
} catch (Exception e) {
LOG.warn("Problems loading " + className, e);
}
}
}
// 序列化完毕
initialized = true;
}
}
在上述代码中核心操作是向authenticationProviders容器加入数据,加入数据的方式有两种。
- 通过new关键字进行手动创建,然后将其放入到容器。
- 通过系统变量进行反射创建,然后将其放入到容器。
最后对getProvider方法进行分析,具体处理代码如下。
public static AuthenticationProvider getProvider(String scheme) {
if (!initialized) {
initialize();
}
return authenticationProviders.get(scheme);
}
这段获取AuthenticationProvider接口实现类的代码操作有两步。
- 如果没有进行过初始化则调用初始化方法。
- 从容器authenticationProviders中获取方案对应的接口。
总结
本章对Zookeeper项目中关于安全认证的内容进行相关分析,主要围绕AuthenticationProvider接口以及该接口的实现类,着重分析这些类中的关键方法。最后对AuthenticationProvider接口的管理类ProviderRegistry进行了相关分析,主要是初始化以及获取两方面的分析。