zookeeper source code analysis

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接口中提供了五个方法分别是。

  1. 方法getScheme用于获取验证方案。
  2. 方法handleAuthentication用于对安全数据进行认证。
  3. 方法matches用于判断id和acl数据信息是否匹配。
  4. 方法isAuthenticated用于判断是否通过身份验证。
  5. 方法isValid用于验证id语法。

在Zookeeper项目中AuthenticationProvider接口存在多个实现类,具体实现类如图所示。

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));
}

在上述代码中处理流程如下。

  1. 判断系统变量中zookeeper.X509AuthenticationProvider.superUser属性是否存在,如果不存在则将id和aclExpr进行字符串相等比较并将其返回。如果存在则进行如下操作。
    1. id和zookeeper.X509AuthenticationProvider.superUser属性进行字符串相等比较。
    2. id和aclExpr进行字符串相等比较。
    3. 上述两个比较结果进行或运算返回。

最后对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;
}

在上述代码中核心处理流程如下。

  1. 从连接对象中获取证书集合,如果证书集合为空或者证书集合长度为0,则返回认证失败状态码。
  2. 如果成员变量trustManager(信任管理器)为空,则返回认证失败状态码。
  3. 从证书集合中获取第一个证书。
  4. 进行安全认证(处理方法trustManager.checkClientTrusted),如果在安全认证中出现CertificateException异常返回认证失败状态码。
  5. 从客户端证书中获取证书id,判断证书id和zookeeper.X509AuthenticationProvider.superUser属性是否相同,相同则创建id对象加入到连接对象中,此时加入的Id对象方案是super,表示超级管理员。
  6. 创建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. 将第(1)步中的字符串进行摘要算法计算。
  3. 若第(2)步的摘要算法结果和超级管理员摘要(超级管理员摘要存储在系统变量zookeeper.DigestAuthenticationProvider.superDigest中)对比,如果相同则创建id对象加入到连接对象中,此时加入的Id对象方案是super,表示超级管理员。
  4. 创建Id对象,将其放入到连接对象中,返回认证通过。
  5. 如果在第(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。上述代码处理流程如下。

  1. 将参数id按照斜杠进行拆分。
  2. 将拆分结果的第一个元素转换为字节数组,如果字节数组为空将返回false。
  3. 如果拆分结果长度为2则需要对bits进行判断,若bits小于0或者bits的数值大于ip字节长度的八倍将返回false,若bits解析为Integer时出现异常也将返回false。
  4. 返回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;
}

在上述代码中对处理流程如下。

  1. 从连接对象中获取host将其命名为id。
  2. 创建Id对象放入到连接对象中。
  3. 返回认证通过。

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;
}

在上述代码中处理流程如下。

  1. 满足下面两个条件中的任意一个返回true。
    1. id和super相同。
    2. id和aclExpr相同
  2. 从环境变量中取出zookeeper.letAnySaslUserDoX属性,判断该属性和aclExpr是否相同如果是则返回true。
  3. 在不满足上述两个条件的情况下返回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容器加入数据,加入数据的方式有两种。

  1. 通过new关键字进行手动创建,然后将其放入到容器。
  2. 通过系统变量进行反射创建,然后将其放入到容器。

最后对getProvider方法进行分析,具体处理代码如下。

public static AuthenticationProvider getProvider(String scheme) {
  if (!initialized) {
    initialize();
  }
  return authenticationProviders.get(scheme);
}

这段获取AuthenticationProvider接口实现类的代码操作有两步。

  1. 如果没有进行过初始化则调用初始化方法。
  2. 从容器authenticationProviders中获取方案对应的接口。

总结

本章对Zookeeper项目中关于安全认证的内容进行相关分析,主要围绕AuthenticationProvider接口以及该接口的实现类,着重分析这些类中的关键方法。最后对AuthenticationProvider接口的管理类ProviderRegistry进行了相关分析,主要是初始化以及获取两方面的分析。