j2se|web|安全
一、 引言
JMX(Java管理扩展)提供了一组工具用来管理本地和远程应用程序、系统对象、设备等。本文将解释如何使用JMX(JSR 160)来远程控制web应用程序,并将解释应用程序中可用于JMX客户的代码,同时将展示使用如MC4J和jManage等的不同客户如何连接到支持JMX的应用程序。此外,我们还将详细地讨论使用RMI协议和JNDI来保护通讯层。
首先我们要分析一个简单的web应用程序,它监控已经登陆的用户数目并通过一个安全的JMX服务来显示该项统计。我们还将运行这个应用程序的多个实例并且从所有的运行实例中跟踪这个统计数字。当然,你可以下载这个示例web应用程序。它需要你安装J2SE 5.0 SDK并且你的JAVA_HOME环境变量指向基安装目录。J2SE 5.0实现了1.2版本的JMX API和JMX 1.0版本的Remote API。同时还需要一个支持servlet的容器;我使用的是Apache Tomcat 5.5.12。另外,我还使用Apache Ant来构建这一示例应用程序。
二、 建立示例应用程序
首先,你要下载示例应用程序并且使用ant war(更多的细节见build.xml中的注释)来创建一个WAR文件。把jmxapp.war复制到Tomcat的webapps目录。假定Tomcat正在运行于你的本地机器的端口8080,那么该应用程序的URL将是:
http://localhost:8080/jmxapp
如果你看到一个提示你输入名字和口令的登陆屏幕,那么一切已经就绪了。
三、 跟踪一些有意义的数据
本文中的应用程序使用Struts框架来提交登录表单。一旦提交结束,即执行LoginAction.execute(..)方法-它将简单地检查是否用户的ID为"hello"以及是否其口令为"world"。如果二者都正确,那么登录成功并且控制被导向login_success.jsp;如果不正确,那么我们返回到登录表单。根据登录成功与否决定调用incrementSuccessLogins(HttpServletRequest)方法还是incrementFailedLogins(HttpServletRequest)方法。现在,让我们先分析一下incrementFailedLogins(HttpServletRequest):
private void incrementFailedLogins(HttpServletRequest request) {
HttpSession session = request.getSession();
ServletContext context =session.getServletContext();
Integer num = (Integer) context.getAttribute( Constants.FAILED_LOGINS_KEY);
int newValue = 1;
if (num != null) { newValue = num.intValue() + 1; }
context.setAttribute( Constants.FAILED_LOGINS_KEY, new Integer(newValue));
}
这个方法增加一个在应用程序范围存储的FAILED_LOGINS_KEY变量。这个incrementSuccessLogins(HttpServletRequest)方法是以相似的方法实现的。该应用程序追踪有多少人成功地登录和有多少人认证失败。这真不错,但是我们该如何存取这些数据?这就是引入JMX的原因。
四、 创建JMX MBeans
MBeans基础知识及其适于JMX架构的方面超出了本文所讨论的范围。我们将为我们的应用程序简单地创建、实现、暴露和保护一个MBean。我们所感兴趣的是暴露相应与下列两个方法的两种数据。下面是我们的简单MBean接口:
public interface LoginStatsMBean {
public int getFailedLogins();
public int getSuccessLogins();
}
这两个方法简单地返回成功和失败登陆的数目。LoginStatsMBean的实现-LoginStats,为上面两种方法提供了一种具体的实现。让我们分析一下getFailedLogins()实现:
public int getFailedLogins() {
ServletContext context = Config.getServletContext();
Integer val = (Integer) context.getAttribute( Constants.FAILED_LOGINS_KEY);
return (val == null) ? 0 : val.intValue();
}
该方法返回一个存储在ServletContext中的值。getSuccessLogins()方法是以相似的方式实现的。
五、 创建和保护一个JMX代理
管理应用程序的JMX相关方面的JMXAgent类有以下几个责任:
1. 创建一个MBeanServer。
2. 用MBeanServer注册LoginStatsMBean。
3. 创建一个JMXConnector以允许远程客户进行连接。
o 包含对JNDI的使用。
o 也必须有一个RMI注册运行。
4. 使用一个用户名和口令保护JMXConnector。
5. 分别在应用程序启动和停止时,启动和停止JMXConnector。
JMXAgent的类轮廓是:
public class JMXAgent {
public JMXAgent() {
//初始化JMX服务器
}
public void start() {
//启动JMX服务器
}
//在应用程序结束时调用
public void stop() {
//停止JMX服务器
}
}
让我们理解在该构造器的这部分代码-它能够使得客户远程地监控该应用程序。
用MBeans创建一个MBeanServer
我们首先创建一个MBeanServer对象。它是JMX基础结构的核心组件,它允许我们暴露我们的MBeans作为可管理的对象。MBeanServerFactory.createMBeanServer(String)方法使得这一任务极为轻松。所提供的参数是服务器的域。可以把它当作这个MBeanServer的唯一的名字。然后,我们用MbeanServe来注册LoginStatsMBean。MBeanServer.registerMBean(Object,ObjectName)方法使用的参数有两个:一个是MBean实现的一个实例;另一个是类型ObjectName的一个对象-它用于唯一地标识该MBean;在这种情况下,DOMAIN+":name=LoginStats"就足够了。
MBeanServer server = MBeanServerFactory.createMBeanServer(DOMAIN);
server.registerMBean(new LoginStats(),new ObjectName(DOMAIN+ ":name=LoginStats")); 六、 创建JMXServiceURL
到现在为止,我们已经创建了一个MBeanServer并且用它注册了LoginStatsMBean。下一步是使得该服务器对客户可用。为此,我们必须创建一个JMXServiceURL-它描述了客户将用来存取该JMX服务的URL:
JMXServiceURL url = new JMXServiceURL("rmi",null,
Constants.MBEAN_SERVER_PORT,
"/jndi/rmi://localhost:" +Constants.RMI_REGISTRY_PORT +"/jmxapp");
让我们细致地分析一下上面一行代码。该JMXServiceURL构造器使用了四个参数:
1. 在连接时使用的协议(rmi,jmxmp,iiop,等等)。
2. JMX服务的主机。用localhost作为参数就足够了。然而,提供null强制JMXServiceURL找到可能是最好的主机名。例如,在这种情况下,它将把null翻译成zarar-这是我的计算机的名字。
3. JMX服务使用的端口。
4. 最后,我们必须提供URL路径-它指示怎样找到JMX服务。在这种情况下,它会是/jndi/rmi://localhost:1099/jmxapp。
其中,/jndi部分是指,客户必须为JMX服务做一下JNDI查询。rmi://localhost:1099指示,存在一个运行于本机的端口1099的RMI注册。这里的jmxapp是在RMI注册中唯一标识这个JMX服务的。在JMXServiceURL对象上的一个toString()产生下列结果:
service:jmx:rmi://zarar:9589/jndi/rmi://localhost:1100/jmxapp
上面是客户将最终使用来连接到该JMX服务的URL。J2SE 5.0文档有关于这个URL结构的更为详细的解释。
(一) 保护服务
J2SE 5.0提供了一种有利于JMX用一种容易的方式进行用户认证的机制。我创建了一个简单的文本文件-它存储用户名和口令信息。文件的内容是:
zarar siddiqi
fyodor dostoevsky
用户zarar和fyodor被分别通过口令siddiqi和dostoevsky认证。下一步是创建并保护一个JMXConnectorServer,它暴露了该MbeanServer。username/password文件的路径被存储在该键下的一个映射中-jmx.remote.x.password.file。这个映射在以后创建JMXConnectorServer时使用。
ServletContext context = Config.getServletContext();
//得到存储jmx用户信息的文件
String userFile =context.getRealPath("/")+"/WEB-INF/classes/"+Constants.JMX_USERS_FILE;
//创建authenticator并且初始化RMI服务器
Map<string> env = new HashMap<string>();
env.put("jmx.remote.x.password.file", userFile);
现在,让我们创建JMXConnectorServer。下面一行代码完成这一功能:
connectorServer = JMXConnectorServerFactory.
newJMXConnectorServer(url, env, server);
这个JMXConnectorServerFactory.newJMXConnectorServer(JMXServiceURL,Map,MBeanServer)方法使用我们刚创建的三个对象作为参数-它们是JMXServiceURL,存储认证信息的映射和MBeanServer。其中,connectorServer实例变量允许我们分别在应用程序启动和停止时,分别用start()和stop()来启动和停止JMXConnectorServer。
提示 尽管JSR 160的J2SE 5.0实现相当有力;但是另外的实现,例如MX4J,也提供了一些类-它们提供了方便的特性,例如口令混淆,也就是PasswordAuthenticator类。
七、 启动RMI注册
在早些时候,我提到RMI注册并且指出当访问服务时执行一个JNDI查询。然而,现在我们没有一个正运行的RMI注册,因此一个JNDI查询将失败。一个RMI注册的启动可以用手工方式或编程方式来实现。
(一) 使用命令行
在你的Windows或Linux命令行上,输入下列一名来启动一个RMI注册:
rmiregistry &
这将启动你的默认主机和端口(分别是localhost和1109)的RMI注册。然而,对于我们的web应用程序来说,我们不可能依赖一个在应用程序启动时可用的RMI而宁愿用编程方式来实现之。
(二) 以编程方式启动RMI注册
为了以编程方式启动RMI注册,你可以使用LocateRegistry.createRegistry(int port)方法。该方法返回类型注册的一个对象。当我们想在应用程序一端终止这个注册时,我们保存这个参考。就在我们启动我们的在JMXAgent.start()中的JMXConnectorServer之前,我们首先启动RMI注册,使用下列代码行:
registry = LocateRegistry.createRegistry(Constants.RMI_REGISTRY_PORT);
在应用程序一端,在JMXAgent.stop()中停止JMXConnectorServer之后,调用下列方法来终止该注册:
UnicastRemoteObject.unexportObject(registry,true);
注意,StartupListener类触发了应用程序开始和结束任务。
八、 访问我们的JMX服务
我们可以有好几种方法来存取JSR 160服务。为此,我们可以通过编程或通过使用一个GUI来实现。
(一) 使用MC4J连接
通过把jmxapp.war复制到Tomcat的webapps目录来发布该应用程序。下载并且安装MC4J。一旦安装完,创建一新的类型JSR 160的服务器连接并且指定该服务器URL-它在应用程序启动时在应用程序服务器日志中打印。在我的示例中,它是:
service:jmx:rmi://zarar:9589/jndi/rmi://localhost:1100/jmxapp
提供用户名和口令,MC4J分别把它们参考为"Principle"和"Credentials"。点击Next将把你带到一个屏幕-在此你可以定制你的classpath。默认设置应该工作正常,并且你可以点击"Finish"来连接到该JMX服务。一旦建立连接,浏览如图1所示的MC4J树结构,直到你找到LoginStats MBean实现的"Properties"选项。
图1.MC4J视图
点击Properties显示统计,如图2所示:
图2.属性窗口
(二) 使用jManage连接到一个"簇"
通过把jmxapp.war复制到Tomcat的webapps目录发布该应用程序。请注意一下在应用程序启动时所打印的URL。接下来,发布这个应用程序的另一个实例-通过改变Constants类中的RMI_REGISTRY_PORT并且MBEAN_SERVER_PORT变量,这样该应用程序的第二个实例就不会试图使用已经在使用的端口了。改变在build.xml文件中的app.name属性,以便新的实例将被发布到一个不同的上下文(例如,jmxapp2)。用ant创建一个清理的war文件-它将在其目录下创建jmxapp2.war。把jmxapp2.war复制到Tomcat的webapps目录。该应用程序将要发布,而且现在你有相同应用程序的两个实例在运行了。我再次提醒你注意在启动时所打印的URL。
下载和安装jManage。一旦安装了,使用jManage的web接口来创建一个JSR 160应用程序-通过使用主页中的"添加新应用程序"链接。"添加应用程序"页面显示在图3中:
图3."添加应用程序"页面
为要发布的第二个应用程序重复前面的步骤并再次使用适当的用户名、口令和URL。。一旦你创建了这两个应用程序,你必须通过遵循在主页中找到的"添加新应用程序簇"链接来创建一个簇。现在,把这两个已经创建的应用程序添加到你的簇上,如图4所示:
图4.添加应用程序簇页面
好了,我们已经完成了!从主页上,点击簇中的一个应用程序,然后点击"Find More Objects"按钮。你将看到name=LoginStats MBean;点击它,则你就会看到我们已经暴露的FailedLogins和SuccessLogins属性。点击在该同一页面上的"Cluster View"链接将显示与图5相类似的一个页面-其中,你可以看到两个应用程序的运行计数统计:
图5.针对jmxapp和jmxapp2的簇视图
试着登录到两个应用程序(http://localhost:8080/jmxapp和http://localhost:8080/jmxapp2)并且观察这些数字是怎样改变的。
九、 结论
现在你已经知道了怎样使你的新的和现有web应用程序支持JMX并且安全地管理它们-使用MC4J和jManage。尽管J2SE 5.0提供了JMX说明书的一个有力的实现,但是另外的开源工程例如XMOJO和MX4J还提供了另外的特征,例如经由web接口甚至更多的方式的连接。如果有兴趣的读者想了解更多地有关JMX的知识,你可以看一下J. Steven Perry写的《Java Management Extensions》一书。如果你对远程应用程序管理感兴趣的话,Jeff Hanson写的《Connecting JMX客户and Servers》将是很有阅读价值的,其中提供了许多真实世界的例子。