使用 Tomcat 和 Apache 分离静态和动态内容

作者:Alan Berg

结合 Apache/SSL 和 Tomcat 托管多个 Java Web 应用程序可能非常复杂。将动态内容与静态内容分离需要 URL 重写和别名。本文讨论了一种可行的配置。

我将描述如何使用纯 Apache 项目方法来托管多个 Java Web 应用程序的基本原理。换句话说,我将解释如何应用 Apache、mod_ssl、一些重写规则和 Tomcat Servlet 容器来获得对一致且可行的生产环境的控制。在现实生活中,我是一位非常繁忙的开发人员,我最近的任务之一是定义和实施一个结构,以在整个生命周期中托管一个复杂的、数据库密集型的、支持 Web 搜索的出版机制。我在此总结获得的经验,并解释最相关的细节。

Daniel McCarthy 在 Linux Journal 网站上的一篇文章中解释了在多个 Tomcat 服务器前放置 Apache Web 服务器的基本原理(请参阅“资源”部分)。我在本文的基础上更进一步,增加了通过 SSL 提供安全通信的能力,并展示了如何通过分离动态内容(如 JSP 页面)和静态内容(如 HTML 和图像)来优化性能。本文还简要提及了进一步的安全问题。

准备工作

以下准备工作适用于那些想要生成所述基础设施的工作实例的人。此基础设施涉及本地配置的 Apache 服务器,该服务器运行两个 Tomcat 实例,所有实例都通过不同的环回 (127.0.0.x) 地址从 Web 浏览器引用。即使不完全按照步骤操作,本文仍然值得一读。

我假设已安装以下组件:Apache 1.3x Web 服务器、mod_ssl、mod_jk 和两个 Tomcat 5.5.x 服务器实例,其中一个在标准端口 8009 上运行 ajp1.3 连接器和关闭端口 8005,另一个在端口 8019 和 8015 上运行。我选择了稳定可靠的旧版 Apache 1.3.x 服务器,而不是 Apache 2.x 版本,原则是不要修复没有损坏的东西。在过去几年我负责的研究所中,他们一直运行 Apache 1.3.x,没有出现任何问题,系统管理员积累了他们的知识,系统维护和修补到最高水平,并稳稳地处于 Web 服务器生命周期的成熟阶段。mod_jk 而不是 mod_jk2 的选择也同样适用。事实上,由于配置的复杂性,mod_jk2 的开发已经停止。

如果您有基于 Debian 的 Linux 发行版,要安装 Apache 服务器而无需编译,请尝试以下操作

sudo apt-get install apache
sudo apt-get install libapache-mod-jk
sudo apt-get install libapache-mod-ssl

现在您应该有一个正在运行的 Apache 实例,其配置文件位于 /etc/apache 下。

对于 Tomcat 服务器,您有两种选择。第一种是使用一个二进制文件实例,然后使用两个配置文件实例,然后运行一个启动脚本,该脚本将唯一的二进制文件实例应用于不同的配置。第二种选择是使用两个 Tomcat 服务器副本并修改 server.xml 文件。第一种方法的优点是避免了可执行代码的复制。然而,这几乎总是一种虚假的节约。第二种方法对于您想要托管不同版本的 Tomcat 服务器的复杂环境具有优势。第二种方法更适用于拥有多个客户的应用程序服务提供商。为 Java 1.5 编写的代码(在 Tomcat 5.5 中本机运行,无需安装 1.4 兼容性包)和在 Tomcat 5 中运行的 Java 1.4 代码之间存在差异。此外,Servlet 实现的版本越新,Tomcat 版本也越新。由于当前变化的速度,托管一年以上的软件可以被认为是遗留软件,因此始终需要使用较旧但仍然可靠的服务器。

接下来,我们只想在环回地址上进行测试,而不会有数据包到达网络。这可以通过修改 /etc/hosts 文件来实现,使其类似于

127.0.0.10 bronze_a
127.0.0.11 silver
127.0.0.12 gold

因此,每次您输入https://bronze_a时,都不需要 DNS 查找。来自浏览器的数据包永远不会到达 Internet,并且将保留在本地 127.0.0.10。

在主 Apache 配置文件 httpd.conf 中,您会找到一个 include 行,该行告诉 Apache 在 conf.d 目录中查找更多配置。例如

Include /etc/apache/conf.d

每次安装需要更改 Apache 配置的软件包时,您都会在 conf.d 目录中找到一个额外的配置文件。事实上,如果您愿意(作为一个有趣的题外话),请尝试安装 Drupal 并阅读转储的 Drupal.conf 文件。

我想将我们的工作与世界其他地方隔离开来。毫无疑问,我们在试验期间会犯错误。添加第二行以包含我们的虚拟主机文件的目录

Include /etc/apache/vhosts

然后,创建目录 /etc/apache/ssl 和 /etc/apache/vhosts。稍后,我们将把我们的证书和服务器密钥放在 SSL 目录中,每个虚拟主机一套。

接下来,检查 httpd.conf 文件以查看 SSL 引擎是否已打开。我想关闭引擎,直到每个虚拟主机启用为止。所以,行SSLEngine On应更改为SSLEngine Off.

现在我们有了一个准备就绪的 Apache 1.3.x 服务器。

如果您尚未设置 Tomcat 服务器,则需要修改第二个实例的 tomcat_root/conf/server.xml 文件下的以下行。将端口号更改为 8015 以用于关闭命令,端口号更改为 8019 以用于 AJP/1.3 连接器

<Server port="8005" shutdown="SHUTDOWN"> 
<Connector port="8080"
<Connector port="8009"
       enableLookups="false"  protocol="AJP/1.3" />

为了安全起见,将 shutdown 属性的值从 SHUTDOWN 更改为一些随机的长字符串。否则,在防御不良的系统中最糟糕的一天,黑客可能会 Telnet 进入并键入SHUTDOWN,然后您的服务器将关闭。此外,我建议注释掉 8080 连接器。没有必要将 Tomcat 直接暴露在 Internet 上。

只剩下一项任务——创建两个 Web 应用程序。在第一个 Tomcat 实例的 webapps 目录下,创建一个 bronze_a 目录,然后在该目录下创建一个 WEB-INF 目录。将以下 web.xml 文件放在 WEB_INF 中

<?xml version="1.0" encoding="ISO-8859-1"?> 
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
    
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
    http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
    version="2.4">

  <display-name>BRONZE_A</display-name>
  <description>
    BRONZE_A Dynamic
  </description>
<welcome-file-list>
 <welcome-file>
index.jsp
 </welcome-file>
</welcome-file-list>
</web-app>

请注意提及 web-app_2_4.xsd。此 web.xml 文件在 Tomcat 5 下不起作用,Tomcat 5 使用的是 2.3 标准。在 webapps/bronze_a 目录下,放置以下 index.jsp 文件。这是我们关于动态内容的简陋但相关的示例

<%String mess="Hello World from Bronze_a"; %>
 <%=mess%> <br><%=request.getRequestURI()%>

对第二个实例执行相同的过程,但在第二个 Tomcat 实例的 webapps/silver 目录下将字符串 bronze_a 替换为 silver。

协同工作

使 Apache 和 Tomcat 服务器相互通信非常简单。如果 httpd.conf 文件中的某个位置尚不存在以下内容,请将以下行添加到文件末尾

JkWorkersFile /usr/local/apache/conf/workers.properties
JkLogFile /usr/local/apache/logs/mod_jk.log
JkLogLevel error

worker.properties 文件的确切位置留给您自行决定。JkLoggFile 和 JkLogLevel 值不是必需的,因为我们将在虚拟主机文件中覆盖它们。但是,为了安心,我喜欢放置默认值,以防以后配置错误。worker 属性定义连接的行为方式。第一行定义 worker 列表——在本例中为 bronze 和 silver。接下来的行是每个 worker 集的配置详细信息。bronze 连接到端口 8008,silver 连接到端口 8019,两个集合都使用 AJ1.3 协议进行通信。这两个 worker 集稍后将在虚拟主机文件中提及

worker.list=bronze,silver

worker.bronze.port=8009
worker.bronze.host=localhost
worker.bronze.type=ajp13

worker.silver.port=8019
worker.silver.host=localhost
worker.silver.type=ajp13
虚拟主机

虚拟主机是通过侦听传入的主机名或 IP 地址在一台机器上托管多个服务器。将多个虚拟主机与 SSL 一起使用仅适用于基于 IP 的虚拟主机。让我用例子来解释。首先,假设我想查看 Web 浏览器和服务器之间的正常事务。为了实现这一点,我使用了相当出色的 Apache SOAP 工具 TcpTunnelGui。为此,首先从 Apache SOAP 网站下载当前存档(请参阅“资源”部分)。展开后,您将看到一个名为 lib 的目录。执行以下操作,如果一切顺利,您将在本地安装 Java 并启动 GUI

cd lib
java -cp ./soap.jar org.apache.soap.util.net.TcpTunnelGui 
 ↪9001 localhost 80

GUI 显示通过端口 9001 的任何 TCP 连接的文本,并将输入重定向回 localhost 80。您可以随意更改 localhost 以指向您自己的测试 Web 服务器。在浏览器中,键入https://:9001。预计会看到以下类型的事务

Accept: */*
Referer: https://:9001
Accept-Language: en-us
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; 
 ↪MSIE 6.0; Windows NT 5.1; CHWIE_NL70; 
 ↪SV1; .NET CLR 1.0.3705; .NET CLR 1.1.4322)
Host: localhost:9001
Connection: Keep-Alive

如您所见,浏览器发送有关自身以及主机和引用变量的信息。Apache 在基于名称的虚拟主机中使用主机变量来确定要应用哪个配置文件。通过键入https://:9001,您将获得类似于以下的乱码响应

?L^A^C?+^A

在 SSL 加密完成之前,Host 变量不可用。因此,每个虚拟主机具有不同的 SSL 证书要求 SSL 进程在配置之前发生。是的,这就是典型的先有鸡还是先有蛋的问题。我们为什么首先需要多个 SSL 证书,从而需要基于 IP 的配置?答案与 SSL 证书中的 cn 属性有关。为了使证书被浏览器接受为有效的服务器证书,cn 属性必须使用目标服务器的主机名值来定义。因此,对于 IP 地址 127.0.0.10,我们需要一个 cn=bronze_a 的证书,对于 IP 地址 127.0.0.11,我们需要一个 cn=silver 的证书。

要使用您自己的本地 CA 生成自签名证书,您需要安装 OpenSSL

sudo apt-cache search openssl
sudo apt-get install openssl

使用以下三个命令生成自签名证书

openssl req -new -out silver.csr
openssl rsa -in privkey.pem -out silver.key
openssl x509 -in silver.csr -out silver.cert 
 ↪-req -signkey silver.key -days 365

第一个命令生成证书请求。请记住,cn 属性必须与虚拟主机中包含的主机名值相同——例如,silver 或 bronze_a。其他属性可以是您认为合理的任何文本值。

第二个命令将密码从新生成的服务器私钥移动到 silver.key,从而删除密码保护。这是必需的;否则,每次重新启动 Apache 时,系统都会要求您在命令行中键入密码。最后一行基于证书请求生成相关证书。将证书和密钥文件都放在 /etc/apache/ssl 目录中。对 bronze_a 执行相同的操作。请记住使用尽可能低的权限来保护 ssl 目录。

要为 bronze 激活端口 80 和 443,请在 vhosts 下添加以下虚拟主机

Listen 127.0.0.10:443
<VirtualHost 127.0.0.10:443>

ServerName bronze_a
Alias /static/ /var/www/customers/bronze_a/content/
RedirectMatch ^/$ https://bronze_a/bronze_a/

SSLEngine On
SSLCertificateFile ssl/bronze_a.cert
SSLCertificateKeyFile ssl/bronze_a.key

JkMount /bronze_a/* bronze
JkMount /bronze_a bronze
JkLogFile /usr/local/apache/logs/bronze_a_mod_jk.log
JkLogLevel info
JkLogStampFormat "[%a %b %d %H:%M:%S %Y] "

RewriteEngine on
  RewriteRule ^/(.*):SSL$ https://%{SERVER_NAME}/$1 [R,L]
  RewriteRule ^/(.*):NOSSL$ http://%{SERVER_NAME}/$1 [R,L]
</VirtualHost>

Listen 127.0.0.10:80
<VirtualHost 127.0.0.10:80>

ServerName bronze_a
Alias /static/ /var/www/customers/bronze_a/content/
RedirectMatch ^/$  https://bronze_a/bronze_a/

JkMount /bronze_a/* bronze
JkMount /bronze_a bronze
JkLogFile /usr/local/apache/logs/80_bronze_a_mod_jk.log
JkLogLevel info
JkLogStampFormat "[%a %b %d %H:%M:%S %Y] "

RewriteEngine on
  RewriteRule ^/(.*):SSL$ https://%{SERVER_NAME}/$1 [R,L]
  RewriteRule ^/(.*):NOSSL$ http://%{SERVER_NAME}/$1 [R,L]

</VirtualHost>

对于 silver,创建一个类似的虚拟主机,但使用 127.0.0.11 IP 地址,并将字符串 bronze_a 替换为字符串 silver。仔细检查您的 SSL 证书和私钥是否指向正确。

使用以下三个小命令打开 SSL

SSLEngine On
SSLCertificateFile ssl/bronze_a.cert
SSLCertificateKeyFile ssl/bronze_a.key

重写规则是从 mod_ssl FAQ 中收集的。发生的情况是您可以控制相对 URL,因此您可以在 SSL 端口和非 SSL 端口之间轻松切换。当您使用 /url:NOSSL 作为 URL 时,URL 将重写为 HTTP 而不是 HTTPS,对于使用 /url:SSL 从 HTTP 重写为 HTTPS 的情况也是如此。

使用以下命令启用 worker 对 Tomcat 服务器的挂载

JkMount /bronze_a/* bronze
JkMount /bronze_a bronze

最好将可能用于调试的日志文件分开——例如

JkLogFile /usr/local/apache/logs/80_bronze_a_mod_jk.log
静态和动态内容共存

Apache 比 Tomcat 更适合交付静态内容、安全性和 URL 重塑。因此,全局性的好处是将站点的静态和动态内容分开,并允许 Apache 通过文件系统处理静态内容,并通过 mod_jk 处理动态内容。URL 重映射的一个实例是顶级重定向。我们已将 mod_jk 挂载在 /bronze_a。如果用户键入http://bronze_a/,他或她要么会找到一个空白页,要么会看到一个漂亮的文件列表。您可以通过在顶层位置放置一个 index.html 页面或通过向下重定向来解决此问题。重定向是通过以下方式实现的

RedirectMatch ^/$  https://bronze_a/bronze_a/

为了确保 uri /bronze_a/ 拾取正确的页面,web.xml 文件中存在以下行

<welcome-file-list>
 <welcome-file>
index.jsp
 </welcome-file>
</welcome-file-list

链接到静态内容的简单方法是在虚拟主机中使用别名。例如,https//bronze_a/static/

Alias /static/ /var/www/customers/bronze_a/content/

开发 Java Web 应用程序往往是一项团队运动。静态内容(例如图像,至少在我的环境中)往往比应用程序本身变化更大。因此,您可以考虑执行显而易见的操作,并将 FTP 根目录设置在静态内容之上,而不是更敏感的动态内容之上。然后,您可以强制 Web 应用程序在将任何新版本投入生产之前进行一系列完整的测试。事实上,您甚至可以考虑混合解决方案。开发人员喜欢通过 CVS 工作。通过将静态和动态内容都放在 war 文件中,您可以将所有代码和内容放在一起,并通过重新安装 war 文件来实现同步部署。这简化了部署,并且系统管理员只需在批准新的属性文件或内容时才执行相同的重复任务。接下来,您需要添加一些 AliasMatch 规则,将某些 URL 视为文件位置,直接分发文件而不是通过 mod_jk,从而避免潜在的性能损失。例如

AliasMatch /web/customers/(.*)/javascript/(.*) 
 /usr/local/tomcat6/webapps/$1/javascript/$2
AliasMatch /web/customers/(.*)/images/(.*) 
 /usr/local/tomcat6/webapps/$1/images/$2
AliasMatch /web/customers/(.*)/css/(.*) 
 /usr/local/tomcat6/webapps/$1/css/$2

这会将 Web 应用程序中 CSS、JavaScript 或图像目录中的文件映射为静态内容。例如,https://xxxxx/web/customers/little.com/javascript/editor.js 转换为 /usr/local/tomcat6/webapps/little.com/javascript/editor.js。

结论

条条大路通罗马,对于猫来说,这非常不幸。本文展示了一种托管 Web 应用程序的方法。我并不声称这是唯一的方法;这仅仅是一种对我有效的方法。我快速地提到了 mod_ssl、mod_jk 以及分离静态和动态内容的一种方法。我希望本文为您提供了足够的信息,让您可以自己尝试测试您的托管概念。通过一些基本配置,控制启用 SSL 的虚拟主机相对简单。

资源

Apache SOAP:ws.apache.org/soap

Daniel McCarthy 的“将 Apache Web 服务器连接到 Tomcat 的多个实例”:www.linuxjournal.com/article/8561

mod_ssl:www.modssl.org

OpenSSL:www.openssl.org

Tomcat 主页:tomcat.apache.org

虚拟主机:httpd.apache.org/docs/1.3/vhosts

Alan Berg,理学士,理学硕士,PGCE,在阿姆斯特丹大学中央计算机服务中心担任首席开发人员已有七年。在业余时间,他撰写计算机文章。他拥有一个学位、两个硕士学位和一个教师资格证。在之前的职业生涯中,他曾担任技术作家、Internet/Linux 课程作家和科学教师。他喜欢亲自动手构建和粘合系统。他通过与孩子们玩电脑游戏来保持敏捷,但(可悲的是)孩子们总是能击败他。您可以通过 reply.to.berg@chello.nl 与他联系。

加载 Disqus 评论