ZooKeeper安全认证机制:用户名密码认证

ZooKeeper提供了简单的基于用户名和密码的认证机制,即DIGEST-MD5认证机制。本文首先介绍使用该认证机制所涉及的一些配置细节,接下来介绍ZooKeeper内部关于DIGEST-MD5认证机制的一些实现细节。

如何使用

Client

系统属性配置:

// "zookeeper.sasl.clientconfig"如果不设置,默认值为"Client"
System.setProperty("zookeeper.sasl.clientconfig",   "Client");
System.setProperty("zookeeper.sasl.client",   "true");

自定义一个JaasConf对象,继承自javax.security.auth.login.Configuration,目的是为了便于Configuration所需参数的配置:

public class JaasConf extends Configuration {
  private Map<String, AppConfigurationEntry[]> sections =
          new HashMap<String, AppConfigurationEntry[]>();

  public void addSection(String name, String loginModuleName, String... args) {
    Map<String, String> options = new HashMap<String, String>();
    for (int i = 0; i < args.length; i += 2) {
      options.put(args[i], args[i + 1]);
    }

    AppConfigurationEntry[] entries = new AppConfigurationEntry[]{
            new AppConfigurationEntry(loginModuleName,
                    AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)};
    this.sections.put(name, entries);
  }

  @Override
  public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
    return this.sections.get(name);
  }
}

实例化JaasConf,设置LoginModuleName以及对应的username/password等信息:

JaasConf conf = new JaasConf();
// Section Name: "Client", 这里的名称与系统属性"zookeeper.sasl.clientconfig"保持一致
// LoginModule Name: "org.apache.zookeeper.server.auth.DigestLoginModule"
// Options:  
//       "username": "nosql"
//       "password": "nosql123"
conf.addSection("Client", "org.apache.zookeeper.server.auth.DigestLoginModule",
            "username", "nosql", "password", "nosql123");
Configuration.setConfiguration(conf);

Server

系统属性配置:

System.setProperty("zookeeper.sasl.serverconfig", "Server");
System.setProperty("zookeeper.authProvider.sasl", 
                   "org.apache.zookeeper.server.auth.SASLAuthenticationProvider");

实例化JaasConf,并在Server端配置所有允许访问的username/password信息:

JaasConf conf = new JaasConf();
// LoginModuleName: "org.apache.zookeeper.server.auth.DigestLoginModule"
// Options:  
//       "user_nosql: nosql123"
conf.addSection("Server", "org.apache.zookeeper.server.auth.DigestLoginModule",
            "user_nosql", "nosql123");
Configuration.setConfiguration(conf);

可以看到,Client端与Server端配置username/password的参数名称是不同的:

  • Client 用户名通过静态参数”username“指定,密码通过静态参数”password“指定
  • Server 用户名直接配置在一个以”user_“开头的动态参数名中,参数值直接为对应的password

Client通过这种模式只能配置一个username/password,而Server端的动态参数则允许配置多个Client的username/password。原因在于,Client只需要配置一个username/password即可,而Server端则允许配置多个Client的username/password

实现原理

整体思路

  • Server端在初始化ServerCnxnFactory时,加载预先配置的允许访问的一个或多个username/password列表,并执行Login操作
  • Client基于配置的username/password以及DigestLoginModule,执行Login操作
  • Client请求与Server端建立Sasl连接,建立连接过程中,通过com.sun.security.sasl.digest.FactoryImpl提供的认证机制,完成对username/password的合法校验

Client初始化

ZooKeeperSaslClient初始化时:

if (login == null) {
  if (LOG.isDebugEnabled()) {
    LOG.debug("JAAS loginContext is: " + loginContext);
  }
  // 初始化Login对象,Login对象是static类型的,也就说,该对象在进程级别内
  // 是共享的. Login对象利用Java JAAS机制执行login操作,具体的Login机制由
  // 配置的LoginContext来实现.
  login = new Login(loginContext, new ClientCallbackHandler(null));
  login.startThreadIfNeeded();
}
Subject subject = login.getSubject();
SaslClient saslClient;
// ZooKeeper支持的认证主要是GSSAPI(Kerberos)以及DIGEST-MD5. 如果基于GSSAPI,
// 认证成功后会在Subject中添加对应的Principal信息. 如果Subject中的Principal
// 信息为空,则认为要使用DIGEST-MD5认证(注: 这种设计并不太好)
if (subject.getPrincipals().isEmpty()) {
  // no principals: must not be GSSAPI: use DIGEST-MD5 mechanism instead.
  LOG.info("Client will use DIGEST-MD5 as SASL mechanism.");
  String[] mechs = {"DIGEST-MD5"};
  // 从subject中获取username与password信息
  String username = (String)(subject.getPublicCredentials().toArray()[0]);
  String password = (String)(subject.getPrivateCredentials().toArray()[0]);
  // 初始化SaslClient时,将username传入,password在ClientCallbackHandler中.
  // "zk-sasl-md5" is a hard-wired 'domain' parameter shared with
  // zookeeper server code (see ServerCnxnFactory.java)
  saslClient = Sasl.createSaslClient(mechs, username, "zookeeper", 
              "zk-sasl-md5", null, new ClientCallbackHandler(password));
  return saslClient;
}

关于如上源码的更多备注信息:

  1. Login阶段,已经配置了LoginModule为org.apache.zookeeper.server.auth.DigestLoginModule
  2. DigestLoginModule中在初始化时已经将Client配置的usernamepassword信息加载到subject中:
    public void initialize(Subject subject, CallbackHandler callbackHandler, 
                           Map<String,?> sharedState, Map<String,?> options) {
         if (options.containsKey("username")) {
           // Zookeeper client: get username and password from JAAS conf 
           // (only used if using DIGEST-MD5).
           this.subject = subject;
           String username = (String)options.get("username");
           this.subject.getPublicCredentials().add((Object)username);
           String password = (String)options.get("password");
           this.subject.getPrivateCredentials().add((Object)password);
         }
         return;
    }
  3. Sasl.createSaslClient的流程:
String mechFilter = "SaslClientFactory." + mechName;
Provider[] provs = Security.getProviders(mechFilter);
for (int j = 0; provs != null && j < provs.length; j++) {
  className = provs[j].getProperty(mechFilter);
  if (className == null) {
    // Case is ignored
    continue;
  }

  fac = (SaslClientFactory) loadFactory(provs[j], className);
  if (fac != null) {
    mech = fac.createSaslClient(
      new String[]{mechanisms[i]}, authorizationId,
      protocol, serverName, props, cbh);
    if (mech != null) {
      return mech;
    }
  }
}

“SaslClientFactory.DEGIEST-MD5″所关联的SaslClientFactory实现为:

com.sun.security.sasl.digest.FactoryImpl

所有的SaslClientFactory的实现信息都被注册在java.security.Security中。

Security与ProviderRegistry:

java.security.Security: Java Security框架中的定义,用来注册SaslClientFactory. 每一个SaslClientFactory都关联着一个Name.

org.apache.zookeeper.server.auth.ProviderRegistry: ZooKeeper中自定义的用来注册所有的AuthenticationProvider的类,每一个AuthenticationProvider关联一个schema

Server端初始化

ServerCnxnFactory#configureSaslLogin中的一些关键源码:

String serverSection = System.getProperty("zookeeper.sasl.serverconfig", "Server");

// Note that 'Configuration' here refers to javax.security.auth.login.Configuration.
AppConfigurationEntry entries[] = null;
SecurityException securityException = null;
try {
  entries = Configuration.getConfiguration().getAppConfigurationEntry(serverSection);
} catch (SecurityException e) {
  // handle below: might be harmless if the user doesn't intend to use JAAS authentication.
  securityException = e;
}
// ...中间略去一下非关键源码....
try {
  // 初始化SaslServerCallbackHandler
  saslServerCallbackHandler = new SaslServerCallbackHandler(Configuration.getConfiguration());
  // 初始化Login对象,利用配置的LoginModule执行login操作.
  login = new Login(serverSection, saslServerCallbackHandler);
  login.startThreadIfNeeded();
} catch (LoginException e) {
  // ....
}

SaslServerCallbackHandler初始化过程中,加载配置的一个或多个username/password信息:

public SaslServerCallbackHandler(Configuration configuration) throws IOException {
  String serverSection = System.getProperty("zookeeper.sasl.serverconfig",
                                            "Server");            
  AppConfigurationEntry configurationEntries[] = 
    configuration.getAppConfigurationEntry(serverSection);

  if (configurationEntries == null) {
    String errorMessage = "Could not find a 'Server' entry in" +
      " this configuration: Server cannot start.";
    LOG.error(errorMessage);
    throw new IOException(errorMessage);
  }
  credentials.clear();
  for(AppConfigurationEntry entry: configurationEntries) {
    Map<String,?> options = entry.getOptions();
    // 所有的用户名都被配置在以"user_"为前缀的属性名中
    for(Map.Entry<String, ?> pair : options.entrySet()) {
      String key = pair.getKey();
      if (key.startsWith(USER_PREFIX)) {
        String userName = key.substring(USER_PREFIX.length());
        credentials.put(userName,(String)pair.getValue());
      }
    }
  }
}

总结

该机制虽然实现了基于用户名和密码的简单认证机制,但所有的用户名和密码信息都是静态配置的,无法支持用户的动态增加,这是该方案的最大软肋。

本文源自:NoSQL漫谈(nosqlnotes.com)
除非特别注明,本站文章均为原创,转载请注明出处和链接。

推荐阅读:

ZooKeeper安全认证机制:SSL

ZooKeeper安全认证机制:ZNode ACL

2 comments

Leave a Reply

电子邮件地址不会被公开。 必填项已用*标注