企业 Java 专家 Dennis Sosnoski 从 Java 服务器技术如何适合 Linux 来开始阐述他的观点,然后给出在 Linux 上安全地设定 Tomcat Java servlet 引擎的一点建议。
Linux 平台和 Java 平台有着久远的但有经常经历曲折的关系。建置高效能虚拟机器的同时又要跟上日益增长的核心 Java API 集合,这样做所带来的复杂性在很大程度上使开发 Java 平台的开放来源程序代码「Clean Room」实作的早期行动困难重重。Java 技术的特许实作最终可用于 Linux,但这些实作并不是开放来源程序代码。因此,大多数 Linux 分发版没有包括该特许实作。
尽管有这些困难,Java 平台还是提供了许多好处,从而导致在 Linux 上越来越多地使用该特许实作,尤其是对于服务器应用程序。在本文中,我回顾了 Java 平台给服务器应用程序带来的优点,然后研究了在 Linux 上简单且安全地部署 Java 服务所涉及的问题。作为一个实际范例,我将讨论设定 Apache Software Foundation 的广泛使用的 Tomcat Java servlet 引擎的详细信息以用于独立作业。
为什么使用 Java 平台?
有许多原因可以解释为什么 Java 平台成为基于服务器的商业应用程序的广为接受的选择。我将主要讨论我认为对于该环境至关重要的三个原因?跨平台兼容性、受管执行时环境和易于开发。
Java 应用程序提供了跨多种操作系统和硬件平台的二进制兼容性。对于非 GUI 服务器应用程序尤其是这样,在此类别应用程序中,通常在实际目标系统中需要执行非常少的测试。工作人员可以在任何他们喜欢的平台上进行编码和除错,同时仍可以将这些应用程序部署到他们也许不能直接控制的环境中。
Java 虚拟机器(Java Virtual Machine,JVM)环境的执行时特性以几种方式来加强程序安全性。最显著的方面之一是严格的类别型检查、数组边界检查和自动垃圾收集的组合彻底防止了最具破坏性形式的服务器程序代码攻击?缓冲区溢位、重复释放的错误和游离的指针。Java 语言早期用于 applet,经过不断发展,该语言还有一个完善的系统,用于对那些已确信存在安全性风险的设施进行细微的存取控制。这些方法可供独立应用程序选择使用,但它们已建置在许多 Java 服务的架构中。
这些执行时程序安全特性还提供了用 Java 语言开发的便利性。要对便利性这类别问题作任何精确测量是困难的,但大多数具有诸如 C 和 C++ 之类别语言背景而转向 Java 程序化的工作人员都承认在转变之后他们的生产力提高了。其中部分是因为在编译时和执行时严格执行类别型确定,以及自动内存管理的简单性。另一个因素是为 Java 平台开发的标准 API 扩充的集合。这些 API 对于新的工作人员可能是一个重大挑战,但是一旦学会了,API 会为各种企业需求提供优秀的跨平台支持。
当然,对于某些应用程序而言,Java 平台可能是一个糟糕的选择。尽管 JVM 体系结构在持续改进,但 Java 应用程序通常会比使用相同算法的 C 或 C++ 应用程序执行得稍微慢一点。根据我的经验和测试,我估计这个速度差异对于在特许 JVM 上执行的大多数服务器应用程序来说大约是在 20% 到 50% 的范围内,然而这很大程度上取决于程序代码的质量。与独立程序相比,在这些 JVM 上执行的 Java 应用程序还忍受着比较慢的启动,但是这对于长时间执行的服务器应用程序通常并不是一个重大问题。在大多数情况中,降低的效能和较慢的启动?是为获得 Java 平台的增强的安全性和更快速的开发优点所付出的微小代价。
开放来源码替代选择
除了标准特许 JVM(免费使用,但是来源码受到限制;可用于 Sun、IBM、BEA 和 Blackdown 组织的 Linux)之外,对于 Linux 还有其它几个替代选择。这些选择包括「Clean Room」开放来源码 JVM 实作,其中使用最广泛的可能是 Kaffe(在许多 Linux 分发版中都包括它)。Kaffe 是一个非常有意义的项目,它已经完成了一些令人惊讶的工作,但它只能提供与目前特许 JVM 有限的兼容性。因此,它通常无法使用于本文所关注的企业类别型的服务器应用程序。
用于 Java 程序的本机程序代码编译器的开放来源码工作也有几个替代选择。这里最重要的项目是 GNU 编译器集(GNU Compiler Collection)的 GCJ。使用诸如 CGJ 之类别的本机程序代码编译器会将独立于平台的 Java 字节码在其执行之前转换成特定于平台的程序代码(这与在 JVM 中执行成对比,在 JVM 中执行通常在执行时将字节码转换成特定于平台的程序代码)。
本机程序代码编译显示出它极有可能成为一种避免在 JVM 中执行的 Java 应用程序启动较慢的方法。但是,使用这种方法的编译器通常都不能与当代特许 JVM 的稳定状态效能相匹配。如果 Java 应用程序使用 Java 平台的动态特性(如使用反射来存取字段或加载在执行时选择的类别),这种情况尤其突出。根据所使用的实作和编译选项,本机程序代码编译也许还会削弱 Java 平台的许多执行时安全特性。最后,由于许可证问题,许多 Java API 不能与已编译的本机程序代码一起使用。由于这些限制,本机程序代码编译目前还不是 Java 平台服务器应用程序的一个好选择。
C# 怎么样?
与 Java 执行时环境有许多共同点的一个替代方法是 Microsoft 的 C# 语言和相关的公共语言执行时(Common Language Runtime,CLR)。C# 是 Java 语言的关系紧密的衍生物,CLR 可能容许 C# 在许多平台上使用。CLR 还提供了 JVM 的许多执行时安全特性(尽管有严重削弱安全保证的逃离出口)。Microsoft .Net 实作还支持预编译成本机程序代码的选项以获得更快速启动,这与 GCJ 对 Java 字节码所做的工作相同。当然,Linux 使用者并不能直接使用这项功能,因为 .Net 只适用于 Windows 系统。
Mono Project 正致力于为多种 Linux 产品建置「CLean Room」开放来源码 C# 和等价于 CLR 的产品。现在,该项目中的 C# 编译器已开发完成,而且还完成了大部分的 CLR,Microsoft 已发布将它用于标准化。但是,无论从效能还是功能角度来看,在它成为合理的 Java 平台替代选择之前还有许多工作要做。CLR 只包括了与 Java 核心类别库等价的基本内容。在可以将它看作是企业软件开发的合理选项之前,还需要用许多额外 API 来补充它。
Mono Project 正在致力于开发 CLR 以外的 .Net 其它部分的移植,如果这些移植成功了 ─ 并且如果 Microsoft 不对 .Net 的这些部分强加它的专利权 ─ 那么它们会有助于满足 C# 成为 Linux 上服务器软件开发的可靠平台的需要。但要使那些假设成为现实,还需要做很多工作,同时,Java 程序的本机程序代码编译器和开放来源码 JVM 向那些确实想要避免使用特许 JVM 并可以忍受有限功能性的使用者提供了比较稳定的替代选择。
Apache Tomcat
最普遍存在的 Java 平台服务器应用程序之一是 Apache Tomcat。Tomcat 是基于最初由 Sun 捐赠的来源码的开放来源码项目。它是一个 HTTP 服务器,是 Sun 透过 Java Community Process 开发的、对广泛使用的 servlet 和 JavaServer Page(JSP)技术的正式参考实作。我将在本文中使用 Tomcat 作为样本 Java 应用程序,将其部署成 Linux 上的一个服务。如果您想要尝试自己执行 Tomcat,那么您将需要在系统上安装 Java 开发工具箱(Java Development Kit,JDK),而不是安装更小的 Java 执行时环境(Java Runtime Environment,JRE)。
servlet 和 JSP 技术用于建构 HTTP 服务器应用程序。虽然 servlet 技术中加入了许多特性(包括存取安全性、Session管理和执行绪控制),但它本身?是粗略地等价于为快速直接的 Java 语言呼叫而定制的 CGI 接口。JSP 技术提供了一种处理动态生成的 HTML 页面的简便方法,这些 HTML 页面被直接编译成 servlet 以用于快速执行时作业。
在这两种技术之外,Tomcat 还提供了其它许多特性。凭它本身的效能,它实际上是全功能 Web 服务器,但它通常在 Linux 系统上与 Apache Web 服务器前端共同使用。Apache 向 Tomcat 提供了许多进阶效能以适合静态内容。对于静态内容所占比例比较高且使用率很高的 Web 应用程序,Apache 前端非常有用。但对于许多简单的 Web 应用程序,就没必要使用它了,当更易于组态和管理时,单独执行 Tomcat 就可提供足够的效能(至少对于以前没有使用过 Apache 的工作人员来说是这样)。
连接埠难题
单独执行 Tomcat 的一个大问题是它无法存取标准 HTTP 连接埠 80,除非是作为 root 使用者执行。作为 root 使用者执行服务器应用程序的想法通常并不是上串流公司所讨论的问题,因此我将完全放弃这个想法!使用除 80 以外的连接埠是一个更好的选择(例如,Tomcat 缺省连接埠 8080)。这通常适用于测试,但当使用者正在存取服务时,它会导致杂乱的 URL,因为需要在请求中清楚地说明连接埠号。使用非标准连接埠还意味着如果需要外部存取,就需要重新组态所有的防火墙。
xinetd 解决方案
幸好,Linux 支持一些利用 Tomcat(或任何其它使用者方式应用程序)处理连接端口 80 请求的简便方式。一种常用方式是透过 xinetd。xinetd 是带有广泛存取控制和日志记录支持的因特网服务守护程序,它还拥有方便的重新导向特性。重新导向让您将系统组态成接受一个连接埠上的进入请求,然后将请求传递到另一个连接埠或者甚至另一个 IP 地址进行处理。
如果您想要在系统上设定 Tomcat 以处理连接埠 80 请求,就需要加入 xinetd 组态文件来实作这一目的。假设按一般在正常路径上安装了 xinetd,那么您可以透过对 /etc/xinetd.d 目录加入一个文件(以 root 使用者身份)来执行这一作业。清单 1 提供了用于 Tomcat 的一个样本组态文件。
清单 1. xinetd 重新导向组态
# Redirects any requests on port 80
# to port 8080 (where Tomcat is listening)
service tomcat
{
socket_type = stream
protocol = tcp
user = root
wait = no
port = 80
redirect = localhost 8080
disable = no
}
在加入了组态文件之后,需要重新启动 xinetd 来真正活化重新导向。在大多数 Linux 安装上,透过以 root 使用者身份执行以下指令来重新启动 xinetd?
/sbin/service xinetd restart
?要将组态文件放在 /etc/xinetd.d 目录中,当重新启动系统时,重新导向就会自动启动。如果没有将 Tomcat 设定成自动启动,那么在启动 Tomcat 之前,会拒绝进入请求。
iptables 解决方案
xinetd 是处理请求重新导向的一种好方法,但它执行了一个处理序以在连接端口之间实际转信数据,这确实增加了一些开销。最新的 Linux 内核版本透过使用 iptables 来支持一种更好的设定重新导向的方法。iptables 与 xinetd 的区别之处在于它是一个真正的内核组件。因此,它可以避免 xinetd 方法增加的开销。使用 iptables 的唯一缺点是它可能比 xinetd 更难以组态,而且它只可用于相当新的内核版本。
您需要执行支持 iptables 的 2.4.x 或更新的内核,以便使用我在这里描述的技术。组态和设定 iptables 是一个确信由几篇文章来单独描述的主题,所以我不打算在这里尝试讨论该主题。如果对 iptables 的入门需要帮助,请阅读 Linux 分发版的手册。要快速检查 iptables 是否在您的系统上执行,尝试以 root 使用者身份执行?
/sbin/service iptables status
如果它正在执行,您将会在控制台上看到表和链的清单。
iptables 使用几个不同的表和封包链来处理规则的。为了将进入 HTTP 请求从连接端口 80 重新导向到系统中的另一个连接埠,您将要使用 nat 表(表示网络地址转换,Network Address Translation)和 PREROUTING 链。清单 2 提供了要执行的实际指令(以 root 使用者身份),以便于加入一条处理这一请求的规则。这条规则的作用是将进入包的目标连接端口 80 修改成目标连接端口 8080,因此?有在您没有阻止从外部使用连接埠 8080 时,这条规则才会正确工作。一旦执行了该指令,您就应该能够立即处理进入请求。
清单 2. iptables 重新导向规则
/sbin/iptables -t nat \
-A PREROUTING -j REDIRECT -p tcp \
--destination-port 80:80 --to-ports 8080
/sbin/service iptables save
以便于储存目前 iptables 组态。
自动启动 Tomcat
当执行诸如 Tomcat 之类别的 Java 服务时的另一个问题是当系统启动时,如何自动启动该应用程序,以及当系统关机时如何自动停止它(换句话说,将它当作守护程序执行)。经验丰富的 Linux 使用者已经知道怎样做,但如果您还是个 Linux 新手,以下就是一些基础知识。
如果您就在您的个人系统上执行它并且想要使用与直接执行 Tomcat 一样的对 Tomcat 的文件和目录的存取权,那么您可以用您自己的使用者名称来设定它。但是,通常,一个比较好的想法是?为将要作为守护程序执行的任何程序设定一个单独的使用者。要针对 Tomcat 执行这一作业,以 root 使用者身份执行?
/usr/sbin/useradd tomcat
这将建立一个名为 tomcat 的使用者账户并建立一个用于 Tomcat 安装的主目录 /home/tomcat。所建立的主目录的所有者是 tomcat 使用者,而且通常只容许这个使用者存取(当然还有 root 使用者)。如果想要从其它账户存取 Tomcat 安装,可以将许可权变更成包括组存取权,并将 tomcat 组新增到这些其它账户。
总之,要将 Tomcat 当作守护程序执行,需要将服务组态文件新增到 /etc/init.d 目录中,而您可能要将该文件命名为「tomcat」。清单 3 提供了该文件的样本。这假设了 Tomcat 安装在 /home/tomcat 下,并且该位置中有两个 shell 指令码文件,用于处理启动和停止服务器(tcstart.sh 和 tcstop.sh)。在执行实际的 Tomcat 启动或停止指令码之前,需要使用这些文件来设定 Tomcat 所需的环境变量(包括 JAVA_HOME 和 JDK_HOME)。
清单 3. Tomcat 服务定义
#!/bin/bash
#
# tomcat Starts Tomcat Java server.
#
#
# chkconfig: 345 88 12
# description: Tomcat is the server for Java servlet applications.
### BEGIN INIT INFO
# Provides: $tomcat
### END INIT INFO
# Source function library.
. /etc/init.d/functions
[ -f /home/tomcat/tcstart.sh ] || exit 0
[ -f /home/tomcat/tcstop.sh ] || exit 0
RETVAL=0
umask 077
start() {
echo -n $"Starting Tomcat Java server: "
daemon su -c /home/tomcat/tcstart.sh tomcat
echo
return $RETVAL
}
stop() {
echo -n $"Shutting down Tomcat Java server: "
daemon su -c /home/tomcat/tcstop.sh tomcat
echo
return $RETVAL
}
restart() {
stop
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
restart
;;
*)
echo $"Usage: $0 {start|stop|restart}"
exit 1
esac
exit $?
以下是一个样本 tcstart.sh,可以修改它来适合您的安装。
清单 4. 样本 tcstart.sh
#!/bin/bash
export JDK_HOME=/usr/java/jdk
export JAVA_HOME=/usr/java/jdk
#run the startup script from Tomcat installation
/home/tomcat/server/bin/startup.sh
chroot?最大的安全性监狱
对于真正的偏执狂,还可能进一步讨论保护 Java 语言服务。当该服务提供对本地文件系统的某种形式的存取权时,这尤其有用。JVM 执行时安全性特性并不能阻止已经有权存取文件系统的应用程序存取除了使用者专用的文件之外的文件。在 Tomcat 的情况中,文件存取是它作为 HTTP 服务器使用时所固有的。它通常将为每个 Web 应用程序提供服务的文件限制为该应用程序目录中的那些文件,但 servlet 应用程序可以避开这些限制。当 Tomcat 与诸如 Apache 之类别的前端 Web 服务器共同执行时,也会发生这种情况。
透过使用 chroot,您可以阻止 Tomcat(和所有在 Tomcat 下执行的 Web 应用程序)存取为该服务器留出的空间以外的任何东西。chroot 并不以任何方式特定于 Java 应用程序,但它是为 JVM 提供的安全性加入最终包装器的简便方法。我将在这里为那些不熟悉 chroot 概念的人介绍设定它的要点。
chroot 所做的类别似于用于执行 Java 程序代码的 JVM 沙箱,但它适用于文件系统本身。chroot 执行一条指令并将您指定的位置设定成有效根目录。所执行的指令(可以是执行其它指令(包括应用程序)的 shell 指令码)只能存取所指定的有效根目录下的文件系统的一部分。文件系统的其余部分对于该指令来说完全不存在。
要对诸如 Tomcat 之类别的应用程序使用 chroot,需要将一些基本系统应用程序和链接库(包括实际的 Java JDK 安装)复制到新的虚拟根目录下。这可能会占用许多空间 ─ 也许从一百兆字节到一千兆字节或更多 ─ 这取决于您想尽多少努力来将这些应用程序和链接库减少到最小。设定它的最简单方法也最浪费空间??要将整个 /bin、/lib、/usr/bin 和 /usr/lib 目录树以及 Java 安装复制到新的根目录下,使 root 使用者成为对所有文件的拥有者和唯一授权的写入者。如果想要让磁盘使用率保持到最小,可以有选择地只复制 chroot 中需要的指令(包括基本指令,如 ls、rm、echo 和 cat 等,以及实际的 Java 安装)以及那些指令使用的库(透过使用 ldd 可以找到那些库)。
接下来,您将需要建立一些额外目录作为一般系统的缩减版本。这包括 /dev,以及设备 /dev/null 和 /dev/zero;/etc 以及 /etc/passwd 和 /etc/group 文件的已编辑版本(只保留 root 和 tomcat 项),也许还有主机。如果正在多处理器系统上执行,您还需要在新的根目录下挂装 /proc 系统,因为 JVM 使用该系统来协调各个处理器。
最后,为了在设定了 chroot 后仍作为使用者 tomcat 执行,也许需要建置一个可以在虚拟根目录下执行的 su 指令版本(许多分发版的一般版本不容许这样做)。要这样做,可以从 GNU 项目获取 sh-utils 来源码并根据该来源码直接建置 su,然后将它复制到新的根目录的 /bin 目录中。
完成了所有这些设定后,可以尝试以 root 使用者身份执行(假设新的根目录位于 /home/tomcat)?
/usr/sbin/chroot /home/tomcat /bin/su tomcat
如果设定正确,这应该让您在新的根目录下作为 tomcat 执行,而且您可以尝试使用您的指令码来启动和停止 Tomcat。在证实了所有东西都工作之后,最后一步就是将清单 3 中的 Tomcat 服务定义变更成使用 chroot 来代替 su,将 su 指令移到 tcstart.sh 指令码和 tcstop.sh 指令码。还需要确保只能由 root 使用者修改这些指令码。
希望这篇概述的解释很清楚,这个过程并不适合胆小的人!如果您选择走使用 chroot 这条路线,但以前没有使用过 chroot,那么您一定要参考网上的 chroot 参考数据之一,以获取详细信息。但是这确实会给您的 Java 服务器程序代码可能最好的隔离,而且在某些情况下,带来的内心的宁静值得这样做。
结语
Linux 和 Java 技术都正在赢得商业系统的市场份额。尽管开放来源码 Linux 和特许 Java 技术之间存在着原理上的差异,但这两者在一起确实配合得很好。Linux 对于 Java 应用程序,尤其是对于服务器类别型的应用程序是一个极好的部署环境,而 Java 技术是作为企业软件开发的先进方法而建立且得到了认可。
透过正确的预防措施,在 Linux 上执行的 Java 服务器应用程序可以提供非常高的安全性程度 ─ 甚至高于本机应用程序 ─ 因为 Java 技术消除了服务器应用程序中许多弱点的常见来源。Java 技术的跨平台性质所带来的是包括企业工作人员和 Linux 立即就绪的应用程序的巨大资源集合。Java 服务器应用程序开始在日益增长的 Linux 的服务器市场份额中扮演重要角色,而且这种趋势在将来只会帮助这两种技术。
我要感谢 Miles Sabin 向我指出了 /proc 文件系统,来解决在多处理器系统上 chroot Java 的问题。