Friday, July 30, 2010

JMX with Spring and Annotation (Part 3)

A few problems I encountered during the whole setup process.
1. In the CustomJMXAuthenticator's authenticate method, my original code was:
public Subject authenticate(Object credentials) {
    if(credentials == null || !(credentials instanceof String[]))
        throw new SecurityException("Credentials are required!");
    String[] info = (String[]) credentials;
    Subject subject = new Subject();
    if(StringUtils.equals(info[0], userName)
        && StringUtils.equals(info[1], password))
        subject.getPrincipals().add(new JMXPrincipal(userName));
    return subject;
}
This works fine in Java 6 Jconsole, incorrect user name and password will result in access deny.  However, in Java 5 Jconsole, CustomJMXAuthenticator becomes useless.  Turns out, in Java 5, as long as a subject is returned, Jconsole will consider the authentication is a success.  So I have to throw a SecurityException to make the authentication work in Java 5.

2. Timing issue in Spring beans.  registry bean has to be fully created before serverConnector bean is being created.  Otherwise, Spring will throw exception at runtime.  For it to work, I have to put registry bean in the top of the context file and serverConnector bean at the bottom.

3. Linux system has a known issue with JMX remote access.  The issue is that when resolving host, if no java.rmi.server.hostname property is defined, it will return ip address 127.0.1.1 instead of 127.0.0.1.  The solution then is set java.rmi.server.hostname to localhost in system property as well as environment map in serverConnector bean.  So aside from the Spring configuration in Part 2.  I also added following code before registry bean:
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="staticMethod" value="java.lang.System.setProperty"/>
    <property name="arguments">
        <list>
            <value>java.rmi.server.hostname</value>
            <value>localhost</value>
        </list>
    </property>
</bean>

JMX with Spring and Annotation (Part 2)

Now that MBeanExporter is setup, we need to set the server-side connector to expose the MBeanServer.

For security concern, I decide to add in JMX authentication.  It is very complicated to setup with Java's configuration.  However, I managed to do so with Spring with two simple steps:
1. CustomJMXAuthenticator.java
package com.my.company.server;

import javax.management.remote.JMXAuthenticator;
import javax.management.remote.JMXPrincipal;
import javax.security.auth.Subject;

import org.apache.commons.lang.StringUtils;

/**
 * @author Yaohua Wang
 *
 */
public class CustomJMXAuthenticator implements JMXAuthenticator {

    private String userName;

    private String password;

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Subject authenticate(Object credentials) {
        if(credentials == null
            || !(credentials instanceof String[]))
            throw new SecurityException("Credentials are required!");

        String[] info = (String[]) credentials;
        if(StringUtils.equals(info[0], userName)
            && StringUtils.equals(info[1], password)){
            Subject subject = new Subject();
            subject.getPrincipals().add(new JMXPrincipal(userName));
            return subject;
        }
        throw new SecurityException("Unable to match credentials!");
    }
}

2. Spring configuration:
<bean id="registry"
    class="org.springframework.remoting.rmi.RmiRegistryFactoryBean"
    destroy-method="destroy" autowire="no">
    <property name="port" value="1099"/>
    <property name="alwaysCreate" value="true"/>
</bean>
...
<bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean" autowire="no" depends-on="registry">
    <property name="objectName" value="connector:name=rmi"/>
    <property name="serviceUrl"
        value="service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi"/>
    <property name="threaded" value="true"/>
    <property name="daemon" value="true"/>
    <property name="environmentMap">
        <map>
            <entry key="java.rmi.server.hostname" value="localhost"/>
            <entry key="jmx.remote.authenticator">
                <bean
class="com.my.company.server.CustomJMXAuthenticator">
                    <property name="userName" value="jmxuser"/>
                    <property name="password" value="jmxpassword"/>
                </bean>
            </entry>
        </map>
    </property>
</bean>

Now start the server and open jconsole.  Type in following information:
  • Remote Process: localhost:1099
  • User Name: jmxuser
  • Password: jmxpassword
You should see the domain "ChannelServer" in MBeans tab.