范围

Geode 可以配置来认证Peer系统成员、客户端、以及远程网关。 Geode 还为客户端对服务器的缓存操作提供授权。 这样就阻止了未经身份验证和用户定义的缓存操作策略访问Geode分布式系统。当Peer成员配置了身份认证,那么需要使用Locator服务来发现 Geode 分布式系统。

这个Geode认证授权子系统是作为一个框架被实现,允许应用打补丁给外部认证服务如LDAP、Kerberos等,我们发现,大多数企业客户通常都有自己的安全基础设施需要整合,如单点登录或集中认证系统。 Geode提供了基于LDAP和PKCS的示例实现,并且提供源代码,用户可以根据自身的要求进行实现或者二次开发。

在使用Geode时也可以使用SSL(安全套接字层)保证数据传输的机密性。

注意:认证和授权管理是提供给加入Geode的分布式系统的每个VM,或者客户端、网关进程。每个成员节点只能代表单一的“principal”或“user”。在连接层面(支持一个进程中多个用户)的身份验证将在未来的版本中添加。

我们首先通过实现简单的认证接口作为安全框架的一部分。然后通过上面描述的实现来配置Geode。示例实现在下面提供了一些描述。在更后面的一部分,展示与Kerberos领域的整合,提供对象级验证更高级的实现。

认证回调接口

认证初始化

第一个需求是Client/Peer实现了Authinitialize接口。这个是需要连接到一个已启用安全的Geode分布式系统的一个Client、Peer或网关的需求(更多的获取安全相关请见“配置Peer之间的认证回调”章节)。

public interface AuthInitialize extends CacheCallback {
  public void init(LogWriter systemLogger, LogWriter securityLogger)
      throws AuthenticationFailedException;
  public Properties getCredentials(Properties securityProps,
      DistributedMember server, boolean isPeer)
      throws AuthenticationFailedException;
}

 

接口里的第一个方法“init”用给定的LogWriters初始化回调。通常一个回调将使用securitylogger为记录的目的,这是一种特殊的记录器,它将标记每行日志以"security-"作为前缀,并且也可以配置到一个单独的日志文件。下面是示例实现:

 

public class SampleAuthInit implements AuthInitialize {
  private LogWriter logger;
  public void init(LogWriter systemLogger, LogWriter securityLogger)
      throws AuthenticationFailedException {
    this.logger = securityLogger;
  }

一个虚拟机获得凭证的方法通过"getCredentials" 方法实现。

参数securityProps 包含Geode特性中的"security-"前缀。 服务器的参数是用于远程服务器的主机、端口和其他成员信息(对于客户端或网关的情况)或Locator/Peer(对于Peer成员的情况下),这将对该成员进行身份验证。当远程成员是Peer或Locator时参数isPeer为true,而当远程成员是一个服务器时参数isPeer为false。这是有用的,如果一个成员既是一个分布式系统中的一个Peer,并且是另一个分布式系统的客户端/网关,这时候该成员需要为这两个系统准备不同的凭证。

一个简单的用户名/密码的实现可以要求“security-username”属性被设置为用户的名称,“security-password”属性被设置为密码(后者通常是程序中设置的),所有需要做的事是传回来的这两个属性如下:

 

public Properties getCredentials(Properties securityProps, DistributedMember server, boolean isPeer)
 throws AuthenticationFailedException {
 Properties credentials = new Properties();
 String userName = securityProps.getProperty("security-username");
 credentials.setProperty("security-username", userName);
 String passwd = securityProps.getProperty("security-password");
 credentials.setProperty("security-password", passwd);
 logger.info("SampleAuthInit: successfully obtained credentials for user " + userName);
 return credentials;}

在上面代码之上加一些检查,我们的示例实现像下面这样:

 

package com.gemstone.samples;
import java.util.Properties;
import com.gemstone.gemfire.LogWriter;
import com.gemstone.gemfire.distributed.DistributedMember;
import com.gemstone.gemfire.security.AuthInitialize;
import com.gemstone.gemfire.security.AuthenticationFailedException;
public class SampleAuthInit implements AuthInitialize {
  private LogWriter logger;
  public static final String USER_NAME = "security-username";
  public static final String PASSWORD = "security-password";
  public static AuthInitialize create() {
    return new SampleAuthInit();
  }
  public void init(LogWriter systemLogger, LogWriter securityLogger)
      throws AuthenticationFailedException {
    this.logger = securityLogger;
  }
  public Properties getCredentials(Properties securityProps,
      DistributedMember server, boolean isPeer)
      throws AuthenticationFailedException {
    Properties credentials = new Properties();
    String userName = securityProps.getProperty(USER_NAME);
    if (userName == null) {
      throw new AuthenticationFailedException(
          "SampleAuthInit: user name property [" + USER_NAME + "] not set.");
    }
    credentials.setProperty(USER_NAME, userName);
    String passwd = securityProps.getProperty(PASSWORD);
    if (passwd == null) {
      throw new AuthenticationFailedException(
          "SampleAuthInit: password property [" + PASSWORD + "] not set.");
    }
    credentials.setProperty(PASSWORD, passwd);
    logger.info("SampleAuthInit: successfully obtained credentials for user "
        + userName);
    return credentials;
  }
  public void close() {
  }
}  

 

上面的实现代码中的“close”方法来自于CacheCallback 接口,是继承AuthInitialize 得到的。 静态方法”create”用来创建一个用来注册回调的接口的实例。

认证器

其次是认证接口,要求实现服务器对一个新的客户端或者Peer的认证。这个回调提供加盟(连接)成员的凭证,该凭证是从AuthInitialize类中getCredentials 方法的属性获得。

public interface Authenticator extends CacheCallback {
 public void init(Properties securityProps, LogWriter systemLogger,
 LogWriter securityLogger) throws AuthenticationFailedException;
 public Principal authenticate(Properties props, DistributedMember member)
 throws AuthenticationFailedException;
}

 

 这有几个方法(除了“close”方法的继于CacheCallback 接口)。第一个“init”是用来执行任何初始化回调提供了LogWriters用于记录。参数 securityProps 提供了所有这些成员的Geode的特性,以前缀“security-”开始。对于我们的示例实现我们将再次使用 securityLogger 提供。

 public class SampleAuthenticator implements Authenticator {
  private LogWriter logger;
  public void init(Properties securityProps, LogWriter systemLogger,
      LogWriter securityLogger) throws AuthenticationFailedException {
    this.logger = securityLogger;
  }

"authenticate"方法是认证客户端或者Peer成员的回调。参数props提供连接成员的认证信息。这和Authinitialize类的getCredentials返回的属性是同样的。最后参数member提供连接客户端或者Peer的会员身份信息。

继续用简单的用户名/密码认证的例子,我们的认证示例使用JAAS(Java认证和授权服务)用户名/密码的实现来验证用户的身份凭据。首先我们需要为JAAS实现 CallbackHandler接口,这样就可以为JAAS登录模块提供用户名/密码 。下面的 SampleCallbackHandler类就是这样处理的。

 package com.gemstone.samples;
import java.io.IOException;
import java.security.Principal;
import java.util.Properties;
import java.util.Set;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import com.gemstone.gemfire.LogWriter;
import com.gemstone.gemfire.distributed.DistributedMember;
import com.gemstone.gemfire.security.AuthenticationFailedException;
import com.gemstone.gemfire.security.Authenticator;
public class SampleAuthenticator implements Authenticator {
  private LogWriter logger;
  private String jaasEntry;
  private LoginContext currentContext;
  public static final String JAAS_ENTRY = "security-jaas-entry";
  public static Authenticator create() {
    return new SampleAuthenticator();
  }
  public void init(Properties securityProps, LogWriter systemLogger,
      LogWriter securityLogger) throws AuthenticationFailedException {
    this.logger = securityLogger;
    this.jaasEntry = securityProps.getProperty(JAAS_ENTRY);
  }
  public Principal authenticate(Properties props, DistributedMember member)
      throws AuthenticationFailedException {
    SampleCallbackHandler callbackHandler = new SampleCallbackHandler(props);
    try {
      this.currentContext = new LoginContext(this.jaasEntry, callbackHandler);
    } catch (LoginException ex) {
      throw new AuthenticationFailedException("SampleAuthenticator: failed "
          + "in creation of LoginContext for JAAS entry: "
          + this.jaasEntry, ex);
    }
    try {
      this.currentContext.login();
    } catch (LoginException ex) {
      throw new AuthenticationFailedException("SampleAuthenticator: "
          + "authentication failed for JAAS entry: " + this.jaasEntry, ex);
    }
    Set<Principal> principals = this.currentContext.getSubject()
        .getPrincipals();
    // assume only one Principal
    if (principals == null || principals.size() != 1) {
      throw new AuthenticationFailedException("SampleAuthenticator: expected "
          + "one Principal but got: " + principals);
    }
    logger.info("SampleAuthenticator: successfully authenticated member: "
        + callbackHandler.userName);
    return principals.iterator().next();
  }
  public void close() {
  }
  class SampleCallbackHandler implements CallbackHandler {
    private final String userName;
    private final String password;
    public SampleCallbackHandler(Properties props) {
      this.userName = props.getProperty(SampleAuthInit.USER_NAME);
      this.password = props.getProperty(SampleAuthInit.PASSWORD);
    }
    public void handle(Callback[] callbacks) throws IOException,
        UnsupportedCallbackException {
      for (Callback callback : callbacks) {
        if (callback instanceof NameCallback) {
          ((NameCallback)callback).setName(this.userName);
        }
        else if (callback instanceof PasswordCallback) {
          ((PasswordCallback)callback).setPassword(this.password.toCharArray());
        }
      }
    }
  }
}

静态方法“create”用于创建用于回调的注册的接口的实例。下面,探讨上述两个回调配置。

 

配置Peer之间的认证回调

 

如上所述,一个Geode分布式系统必须使用Locators寻找设置了安全的Peer。这与下面讨论的多路广播寻找设置了安全的Peer不相同。

一个Peer想要加入一个安全的分布式系统,必须呈现它的身份认证给一个已经认证的Locator。假定第一个加入到分布式系统的Locator被认证了,而后面的都没有。Locator列表是从Geode的"locators"属性获得。从AuthIntialize类的getCredentials 方法获得的身份证明被发送到一个Locator进行认证。在所有的Geode成员中security-peer-auth-init属性在一个返回AuthInitialize 对象的无参静态方法中应被设置成名字,而在所有成员和定位器中security-peer-authenticator属性在一个返回Authenticator对象的无参静态方法中应被设置成名字。注意由于成员认证的查看消息已经发出去,所以除了Locator所有成员还需要配置认证。

上面的示例实现所需的设置:

security-peer-auth-init – com.gemstone.samples.SampleAuthInit.create

security-username – a valid user name

security-password – password for the above user

security-peer-authenticator – com.gemstone.samples.SampleAuthenticator.create

_security-jaas-entry _– entry name in JAAS configuration file to use for authentication

JAAS配置文件应该使用正常系统属性"java.security.auth.login.config"。这些都需要在所有的Geode服务器和分布式系统的定位。示例代码来编程方式如下:

    Properties props = new Properties();
    props.setProperty("security-peer-auth-init",
        "com.gemstone.samples.SampleAuthInit.create");
    props.setProperty(SampleAuthInit.USER_NAME, "user1");
    props.setProperty(SampleAuthInit.PASSWORD, "xxx");
    props.setProperty("security-peer-authenticator",
        "com.gemstone.samples.SampleAuthenticator.create");
    props.setProperty("security-jaas-entry", "Sample");
    DistributedSystem sys = DistributedSystem.connect(props);

 

如果一个Peer成员或Locator认证失败,那么DistributedSystem.connect()方法抛出一个 AuthenticationFailedException。如果Locator/Peer 有设置security-peer-authenticator 属性但成员没有 设置security-peer-auth-initproperty属性,然后会抛出AuthenticationRequiredException。所有的安全异常有 GemFireSecurityException 为基类,所以用户代码可以在需要的地方选择捕获基类的异常。

 

配置Geode客户端和服务器之间的认证回调

 

客户端被认证是从与Geode缓存服务器的每一次握手开始,也是每一次客户端到服务器的TCP连接。在握手期间客户端递交它的身份认证信息,然后服务器用这些信息验证客户端。客户端必须信任那些和它的endpoints 列表中绑定的地址和端口号 host:bind-address[port] 一样的缓存服务器,因为客户端可以连接到列表中的任何服务器并且传递它的身份认证信息,那些从AuthInitialize类的getCredentials方法获得的身份认证信息被发送服务器进行认证。在所有的客户端上security-peer-auth-init属性在一个返回AuthInitialize 对象的无参静态方法中应该被设置成名字,而在所有的服务器上security-client-authenticator属性在一个返回Authenticator 对象的无参静态方法中应该被设置成名字。

上面的示例实现所需的设置:

对于客户端:

security-client-auth-init – com.gemstone.samples.SampleAuthInit.create

security-username – a valid user name

security-password – password for the above user

对于服务器:

security-client-authenticator – com.gemstone.samples.SampleAuthenticator.create

security-jaas-entry – entry name in JAAS configuration file to use for authentication

JAAS配置文件应该使用正常系统属性"java.security.auth.login.config"。这些都需要在所有的Peer服务器和分布式系统的Locators。示例代码来编程方式如下:

对于客户端:

    Properties props = new Properties();
    props.setProperty("security-client-auth-init",
        "com.gemstone.samples.SampleAuthInit.create");
    props.setProperty(SampleAuthInit.USER_NAME, "user1");
    props.setProperty(SampleAuthInit.PASSWORD, "xxx");
    DistributedSystem sys = DistributedSystem.connect(props);

对于服务器:

 Properties props = new Properties();
 props.setProperty("security-client-authenticator",
 "com.gemstone.samples.SampleAuthenticator.create");
 props.setProperty("security-jaas-entry", "Sample");
 DistributedSystem sys = DistributedSystem.connect(props);

不像Geode服务器,客户端的认证表现为在每一次客户端和服务器之间连接,这些连接都是在Region操作期间动态创建的。如果客户端认证失败,那么Region API方法需要在服务器抛出AuthenticationFailedException异常。如果服务器有security-client-authenticator属性设置,但是客户端没有security-client-auth-init设置,就会在Region API方法中抛出AuthenticationRequiredException 异常。

认证回调接口

首先对服务器进行身份验证的当前客户端提供缓存操作的授权。首先一旦客户端认证了服务器,Authenticator#authenticate方法返回的Principal 对象与客户端相关。如果有,然后传递给服务器上的授权回调。

有两个授权缓存操作的地方:一个在操作前的预备操作阶段,另一个在后置操作阶段,也就是在服务器完成认证操作之后,返回结果给客户端之前(返回的结果是查询的操作类型)。此外,被服务器发送到客户端的通知也在后置操作阶段被授权。

public interface AccessControl extends CacheCallback {
 public void init(Principal principal, DistributedMember remoteMember,
 Cache cache) throws NotAuthorizedException;
 public boolean authorizeOperation(String regionName, OperationContext context);
}

“init”方法被调用了用来为客户端初始化回调。Principal 对象获得了授权过的客户端(Authenticator#authenticate方法的返回值),它被作为这个方法的第一个参数。客户端的会员信息在参数remoteMember 中有提供,同时第三个参数Cache 提供Geode缓存。当在缓存授权连接操作的时候”authorizeOperation”方法会被执行。该方法需要提供操作的region的名字和封装了当前缓存操作的OperationContext 对象。

继续我们的用户名/密码示例,我们假设在上面的示例实现中JAAS授权模块返回Principal (自定义的Principal 对象),它提供”isReader”方法,如果成员只被赋予只读权限,该方法返回true,而其他成员拥有全部权限,预授权回调的代码如下:

package com.gemstone.samples;
import java.security.Principal;
import com.gemstone.gemfire.cache.Cache;
import com.gemstone.gemfire.cache.operations.OperationContext;
import com.gemstone.gemfire.cache.operations.OperationContext.OperationCode;
import com.gemstone.gemfire.distributed.DistributedMember;
import com.gemstone.gemfire.security.AccessControl;
import com.gemstone.gemfire.security.NotAuthorizedException;
public class SampleAccessControl implements AccessControl {
 private boolean isReader;
 public static AccessControl create() {
 return new SampleAccessControl();
 }
 public void init(Principal principal, DistributedMember remoteMember,
 Cache cache) throws NotAuthorizedException {
 if (principal instanceof MyPrincipal) {
 this.isReader = ((MyPrincipal)principal).isReader();
 }
 else {
 this.isReader = false;
 }
 }
 public boolean authorizeOperation(String regionName, OperationContext context) {
 if (this.isReader) {
 OperationCode opCode = context.getOperationCode();
 // these cache operations do not modify data
 return (opCode.isGet() || opCode.isQuery() || opCode.isContainsKey()
 || opCode.isKeySet() || opCode.isRegisterInterest()
 || opCode.isUnregisterInterest() || opCode.isExecuteCQ()
 || opCode.isCloseCQ() || opCode.isStopCQ());
 }
 else {
 return true;
 }
 }
 public void close() {
 }
}

 

“init”方法为客户端缓存了”isReader”方法的标识,因为isReader方法可能有潜在风险。当isReader标识为true时authorizeOperation方法然后才允许只读操作

授权回调配置

服务器的设置要求如上面的示例实现所示:

security-client-access-control – com.gemstone.samples.SampleAccessControl.create

如果为一个被服务器拒绝的缓存操作授权(例如authorizeOperation方法在服务器上返回false),那么这个客户端会因为这个操作收到一个NotAuthorizedException 异常。

阿德翻译

  • No labels