首先要从JMX URL说起。如下是一个典型的JMX URL:

service:jmx:rmi://localhost:5000/jndi/rmi://localhost:6000/jmxrmi

这个JMX URL可以分为如下几个部分:

  • service:jmx: 这个是JMX URL的标准前缀,所有的JMX URL都必须以该字符串开头。
  • rmi: 这个是connector server的传输协议,在这个url中是使用rmi来进行传输的。JSR 160规定了所有connector server都必须至少实现rmi传输,是否还支持其他的传输协议依赖于具体的实现。比如MX4J就支持soap、soap+ssl、hessian、burlap等等传输协议。
  • localhost:5000: 这个是connector server的IP和端口,该部分是一个可选项,可以被省略掉。(具体参见下文解释)
  • /jndi/rmi://localhost:6000/jmxrmi: 这个是connector server的路径,具体含义取决于前面的传输协议。比如该URL中这串字符串就代表着该connector server的stub是使用jndi api绑定在rmi://localhost:6000/jmxrmi这个地址。

如果在服务器端,我们用该URL创建一个connector server,则大概流程如下:

  1. 将connect server(javax.management.remote.rmi.RMIConnectorServer)内部的server对象 (javax.management.remote.rmi.RMIJRMPServerImpl)的rmi stub export到本地的5000端口,接收外部连接
  2. 通过jndi api将该stub绑定在rmi://localhost:6000/jmxrmi这个地址上,这需要在本地的6000端口上运行着一个rmiregistry,如果不存在则会抛出异常
  3. ……

可以看到,如果在服务器端创建connector server时,该URL的第三部分(即localhost:5000)如果省略的话,则connector server会随机任意选择一个可用的端口。

如果在客户端,我们通过该URL创建一个connector,则大概按照如下的流程:

  1. 通过jndi api到rmi://localhost:6000/jmxrmi这个地址取回stub
  2. stub中已经包含了真实服务器的地址,所以可以直接根据该stub连接到真实的服务器
  3. ……

可以看到,如果在客户端创建connector时,该URL的第三部分可以被省略掉。事实上我们也经常这么做,比如上面这个示例,在客户端建立connector所用的JMX URL可以被省略为:

service:jmx:rmi:///jndi/rmi://localhost:6000/jmxrmi

从 这个示例中我们也可以看到,如果采用rmi作为传输协议的话,客户端需要进行两个连接。首先客户端连接到rmiregistry上得到真实服务器的 stub(如rmi://localhost:6000/rmxrmi),然后客户端再根据该stub连接到真实的服务器上(如rmi: //localhost:5000)。

 

在Sun Java 5.0的实现当中,可以通过指定运行期属性来配置JMX。比如:

-Dcom.sun.management.jmxremote.port=6000
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.password.file=conf/jmxremote.password
-Dcom.sun.management.jmxremote.access.file=conf/jmxremote.access

除开密码和权限控制的配置外,这段运行期相当于一个如下的JMX URL:

service:jmx:rmi://localhost:0/jndi/rmi://localhost:6000/jmxrmi (红色部分可以被省略掉)

可 以看到,通过指定com.sun.management.jmxremote.port属性,相当于指定了rmiregistry的运行端口,但是真正运 行的服务器的stub export端口为0,会在运行期任意选择一个未被使用的端口号。如果该程序运行在防火墙后面,这个动态分配的端口号会让我们非常难以配置防火墙规则。

由于Sun Java 5.0实现的疏忽,并没有为我们提供一个运行期属性来配置stub export端口号,那么如何自定义一个JMX连接端口呢?

假设我们不传入任何运行期参数,仅通过代码来自定义一个connector server:

LocateRegistry.createRegistry(6000);
JMXConnectorServer cs = JMXConnectorServerFactory
.newJMXConnectorServer(url, env, beanServer);
cs.start();

第一行代码指明在6000端口创建一个rmiregistry。

第 二行代码创建了一个connector server。第一个URL参数传入指定的JMXServiceURL,比如new JMXServiceURL(“service:jmx:rmi://localhost:5000/jndi/rmi: //localhost:6000/jmxrmi”),值得注意的是红色部分标出的IP和端口号要和第一步中创建的rmiregistry一致。第二个参 数指定创建时的环境,我们可以暂时传入一个空Map。第三个参数指定要创建连接的MBeanServer,比如在Java 5.0下可以是ManagementFactory.getPlatformMBeanServer()。

第三行代码开启了创建的connector server。

这三行代码创建的connector server工作的很好,除了一点:真实的服务器没有任何认证和授权机制,会允许所有人访问。

自定义权限控制可以通过刚才第二行代码的第二个参数来进行设置:

Map env = new HashMap();
env.put(JMXConnectorServer.AUTHENTICATOR, JMXAuthenticator子类的实例);

比 如在MX4J中就提供了一个mx4j.tools.remote.PasswordAuthenticator的类,通过配置文件来匹配用户名和密 码;Sun也提供了一个com.sun.jmx.remote.security.JMXPluggableAuthenticator的类通过JAAS 来配置验证和权限。用户可以参照这两个类来提供自定义的实现。这里提供一个简单的示例(不带任何效验机制,仅用于示例)

env.put(JMXConnectorServer.AUTHENTICATOR, new JMXAuthenticator() {

public Subject authenticate(Object credentials) {
String[] sCredentials = (String[]) credentials;
String userName = sCredentials[0];
String password = sCredentials[1];
if (“test”.equals(userName) && “test”.equals(password)) {
Set principals = new HashSet();
principals.add(new JMXPrincipal(user));
return new Subject(true, principals, Collections.EMPTY_SET,
Collections.EMPTY_SET);
}
throw new SecurityException(“Authentication failed! ”
+ message);

}
});

加 入这段代码后,真实的服务器必须要验证后才能访问,但又有了一个新问题:rmiregistry没有任何验证。这意味着其他人可以把一个不相干的stub 绑定到同一地址,从而替换掉真实服务器stub。好在幸运的是,无法把一个服务器端类路径上不存在的stub类绑定到rmiregistry,否则真实的 用户名和密码就有可能被泄漏出去。

如果直接通过rmi对外提供的接口,我们没有办法修正这个问题,除非将代码绑定于特定的Java实现,比 如Sun就直接通过它自己的rmi实现类来做(所以通过运行期参数配置jmx生成的rmiregistry是只读的,没有该安全性问题)。具体可参见 sun.management.Agent和sun.management.jmxremote包下相关类。

但是我们可以换一个思路,比如 不绑定到rmiregistry而绑定到其他的实现了验证机制的jndi provider,例如ldap上。由于bind jndi的代码在相关connector server的实现类中,比如javax.management.remote.rmi.RMIConnectorServer中bind jndi的代码:

void bind(String jndiUrl, Hashtable attributes,
RMIServer rmiServer, boolean rebind)
throws NamingException, MalformedURLException {
// if jndiURL is not null, we nust bind the stub to a
// directory.
InitialContext ctx =
new InitialContext(attributes);

if (rebind)
ctx.rebind(jndiUrl, rmiServer);
else
ctx.bind(jndiUrl, rmiServer);
ctx.close();
}

它在传入attributes时并没有特定的jndi的INITIAL_CONTEXT_FACTORY,所以我们可以通过两种方法来指定特定的INITIAL_CONTEXT_FACTORY:

  • 在类路径上放上一个jndi.properties,配置相应的jndi属性
  • 在start connector server前,先在系统属性中指定相应的jndi属性,start后再把这些属性恢复成原有属性

其中第一种方式不需要额外的代码,但是可能会对系统中其他jndi的InitialContext产生影响;第二种方式稍微麻烦点,但是在实现上比较完美。

值得提到的是,如果采用rmiregistry,并且rmiregistry和server在同一个JVM内,stub和rmiregistry可以共享一个端口。比如可以使用如下的JMX URL:

service:jmx:rmi://localhost:6000/jndi/rmi://localhost:6000/jmxrmi

但注意一定要在同一JVM内,比如先用LocateRegistry.createRegistry(6000)创建本地rmiregistry,再通过该URL开启connector server,则可以共享6000端口。

 

Leave a Reply

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