Apache HTTP 服务器模块 mod_jk 及其用于 Microsoft IIS 的 ISAPI 重定向器变体使用 AJP 协议将 Web 服务器连接到后端(通常是 Tomcat)。Web 服务器接收 HTTP(S) 请求,模块将请求转发到后端。此功能通常称为网关或代理,在 HTTP 上下文中称为反向代理。
反向代理操作指南
简介
典型问题
反向代理对后端应用程序并非完全透明。例如,原始客户端(例如浏览器)需要与之通信的主机名和端口属于 Web 服务器,而不是后端,因此反向代理与不同的主机名和端口通信。当后端应用程序返回包含使用其自身后端地址和端口的自引用 URL 的内容时,客户端通常无法使用这些 URL。
另一个例子是客户端 IP 地址,对于 Web 服务器来说,它是传入连接的源 IP,而对于后端来说,连接总是来自 Web 服务器。当后端应用程序使用客户端 IP 地址时,例如出于安全原因,这可能是一个问题。
AJP 作为解决方案
AJP 协议和后端的 AJP 连接器自动处理了大多数这些问题。AJP 协议传输此通信元数据,后端连接器在应用程序使用 Servlet API 方法请求时呈现此元数据。
以下列表包含 AJP 处理的通信元数据以及可用于检索它们的 ServletRequest/HttpServletRequest API 调用
- 本地名称:
getLocalName()
。这也等于getServerName()
,除非请求中包含Host
标头。在这种情况下,服务器名称取自该标头。 - 本地 IP 地址:
getLocalAddr()
。最初不支持本地 IP 地址。当使用 Apache 的 1.2.41 版本或 IIS 与 Tomcat 版本至少为 6.0.42、7.0.55 或 8.0.11 一起使用时,它才可用。对于旧版本或使用 NSAPI 重定向器时,getLocalAddr()
将错误地返回与getLocalName()
相同的结果。作为解决方法,您可以通过设置JkEnvVar SERVER_ADDR
来转发本地 IP 地址,然后使用request.getAttribute("SERVER_ADDR")
代替getLocalAddr()
,或者使用过滤器包装请求并使用request.getAttribute("SERVER_ADDR")
覆盖getLocalAddr()
。 - 本地端口:
getLocalPort()
。这与getServerPort()
相同,除非请求中包含Host
标头。在这种情况下,如果服务器端口包含显式端口,则从该标头获取服务器端口,否则等于所用方案的默认端口。 - 客户端地址:
getRemoteAddr()
- 客户端端口:
getRemotePort()
。最初不支持远程端口。当使用 Apache 的 1.2.32 版本或 IIS 与 Tomcat 版本至少为 5.5.28、6.0.20 或 7.0.0 一起使用时,它才可用。对于旧版本或使用 NSAPI 重定向器时,getRemotePort()
将错误地返回 0 或 -1。作为解决方法,您可以通过设置JkEnvVar REMOTE_PORT
来转发远程端口,然后使用request.getAttribute("REMOTE_PORT")
代替getRemotePort()
,或者使用过滤器包装请求并使用request.getAttribute("REMOTE_PORT")
覆盖getRemotePort()
。 - 客户端主机:
getRemoteHost()
- 身份验证类型:
getAuthType()
- 远程用户:
getRemoteUser()
,如果tomcatAuthentication="false"
- 协议:
getProtocol()
- HTTP 方法:
getMethod()
- URI:
getRequestURI()
- 是否使用 HTTPS:
isSecure()
、getScheme()
- 查询字符串:
getQueryString()
SSLOptions +StdEnvVars
时由 mod_jk 转发。对于证书信息,您还需要设置 SSLOptions +ExportCertData
。
- SSL 密码:
getAttribute(javax.servlet.request.cipher_suite)
- SSL 密钥大小:
getAttribute(javax.servlet.request.key_size)
。可以使用JkOptions -ForwardKeySize
禁用。 - SSL 客户端证书:
getAttribute(javax.servlet.request.X509Certificate)
。如果您想要整个证书链,则还需要设置JkOptions ForwardSSLCertChain
。在这种情况下,您可能还需要使用 worker 属性 max_packet_size 调整最大 AJP 数据包大小。 - SSL 会话 ID:
getAttribute(javax.servlet.request.ssl_session)
。这是针对 Tomcat 的,尚未标准化。
微调
但在某些情况下,这还不够。假设您的 Web 服务器前面还有另一个不太智能的反向代理,例如 HTTP 负载均衡器或类似设备,它也充当 SSL 加速器。
那么您确定所有客户端都使用 HTTPS,但您的 Web 服务器对此一无所知。它只能看到来自加速器使用纯 HTTP 的请求。
另一个例子是在您的 Web 服务器前面有一个简单的反向代理,这样您的 Web 服务器看到的客户端 IP 地址始终是此反向代理的 IP 地址,而不是原始客户端的 IP 地址。通常,此类反向代理会生成一个额外的 HTTP 标头,例如 X-Forwareded-for
,其中包含原始客户端 IP 地址(或 IP 地址列表,如果前面有更多级联反向代理)。如果我们可以使用此标头的内容作为客户端 IP 地址传递给后端,那就太好了。
因此,我们可能需要操作 AJP 发送到后端的一些数据。在 Apache HTTP Server 中使用 mod_jk 时,您可以使用几个 Apache 环境变量来让 mod_jk 知道它应该转发哪些数据。这些环境变量可以通过配置指令 SetEnv 或 SetEnvIf 设置,也可以通过使用 mod_rewrite 以非常灵活的方式设置(从 Apache 2.x 开始,它不仅可以针对环境变量进行测试,还可以设置它们)。
以下列表包含 mod_jk 在将数据发送到后端之前检查的所有环境变量
- JK_LOCAL_NAME:本地名称
- JK_LOCAL_PORT:本地端口
- JK_REMOTE_HOST:客户端主机
- JK_REMOTE_ADDR:客户端地址
- JK_AUTH_TYPE:身份验证类型
- JK_REMOTE_USER:远程用户
- HTTPS:On(不区分大小写)表示使用 HTTPS
- SSL_CIPHER:SSL 密码
- SSL_CIPHER_USEKEYSIZE: SSL 密钥大小
- SSL_CLIENT_CERT: SSL 客户端证书
- SSL_CLIENT_CERT_CHAIN_: 变量名称前缀,包含客户端证书链
- SSL_SESSION_ID: SSL 会话 ID
请记住:通常您不需要设置它们。模块会自动从 Web 服务器检索数据。只有在您想要更改此数据时,才能使用这些变量覆盖它。
其中一些变量也可能被其他 Web 服务器模块使用。所有名称不以“JK”开头的变量都由 Apache HTTP 服务器直接设置。如果您想更改数据,但不想对其他模块的行为产生负面影响,您可以将 mod_jk 使用的所有变量的名称更改为私有变量。有关详细信息,请参见 Apache 参考 页面。
所有与 SSL 无关的变量仅在 1.2.27 版本中引入。
此外,还有两个特殊的快捷方式可以影响转发客户端 IP 地址。使用 JkOptions ForwardLocalAddress
,您可以将 Web 服务器的本地 IP 地址转发为客户端 IP 地址。这在使用 Tomcat 远程地址阀门仅允许来自注册的 Apache HTTP 服务器的连接时非常有用。使用 JkOptions ForwardPhysicalAddress
,您始终将物理对等 IP 地址转发为客户端地址。默认情况下,mod_jk 使用 Web 服务器提供的逻辑地址。例如,模块 mod_remoteip 将逻辑 IP 地址设置为代理在 X-Forwarded-For
标头中转发的客户端 IP 地址。
Tomcat AJP 连接器设置
作为使用上一节中描述的环境变量(这些变量仅在使用 Apache 时存在)的替代方法,您还可以配置 Tomcat 以覆盖 mod_jk 转发的某些通信数据。Tomcat 的 server.xml
中的 AJP 连接器允许设置 以下属性
- proxyName: 由
getServerName()
返回的服务器名称 - proxyPort: 由
getServerPort()
返回的服务器端口 - scheme: 由
getScheme()
返回的协议方案 - secure: 如果希望
isSecure()
返回“true”,则设置为“true”。
URL 处理
URL 重写
有时,人们希望更改应用程序可用的 URL 的路径组件。特别是如果 Web 应用程序作为某个上下文(例如 /myapp
)部署,营销人员更喜欢使用简短的 URL,因此希望应用程序能够直接在 http://www.mycompany.com/
下使用。虽然您可以将应用程序部署为所谓的 ROOT 上下文,该上下文将直接在“/”下可用,但管理员通常更喜欢不使用 ROOT 上下文,例如,因为只有一个应用程序可以是 ROOT 上下文(每个主机)。
在反向代理中更改 URL 的过程很繁琐,因为应用程序通常会生成自引用的 URL,然后这些 URL 包含您试图隐藏到外部世界的路径组件。但是,如果您绝对需要这样做,请按照以下步骤操作。
案例 A:您需要在简单的 URL 上提供应用程序,但如果用户使用更复杂的 URL 继续访问,只要他们不需要手动输入这些 URL 即可。这是最简单的案例,如果这对您足够了,您很幸运。使用 Apache HTTP 服务器的简单 RedirectMatch
RedirectMatch ^/$ http://www.mycompany.com/myapp/
然后,您的应用程序将在 http://www.mycompany.com/
下可用,每个访问者都会立即被重定向到实际 URL http://www.mycompany.com/myapp/
案例 B:您需要隐藏所有访问应用程序的请求的路径组件。以下是您想要隐藏第一个路径组件 /myapp
的案例的解决方案。更复杂的处理留给读者作为练习。首先是 Apache HTTP 服务器的解决方案
1. 使用 mod_rewrite
在转发到后端之前将 /myapp
添加到所有请求中
# Don't forget the PT flag! (pass through)
RewriteRule ^/(.*) http://www.mycompany.com/myapp/$1 [PT]
2. 使用 mod_headers
重写应用程序可能返回的任何 HTTP 重定向。此类重定向通常包含您要隐藏的路径组件,因为根据 HTTP 标准,重定向始终需要包含完整的 URL,而您的应用程序不知道您的客户端通过一些缩短的 URL 与其通信。HTTP 重定向使用名为 Location
的特殊响应头完成。我们重写了响应的 Location 头
# Keep protocol, server and port if present,
# but insert our webapp name before the rest of the URL
Header edit Location ^([^/]*//[^/]*)?/(.*)$ $1/myapp/$2
3. 再次使用 mod_headers
重写应用程序可能设置的任何 cookie 中包含的路径。此类 cookie 路径可能再次包含您要隐藏的路径组件。cookie 使用名为 Set-Cookie
的 HTTP 响应头设置。我们重写了响应的 Set-Cookie 头
# Fix the cookie path
Header edit Set-Cookie "^(.*; Path=/)(.*)" $1/myapp/$2
3. 一些应用程序可能包含硬编码的绝对链接。在这种情况下,请检查您是否找到了 Web 框架的配置项来配置基本 URL。如果没有,您唯一的选择是解析所有响应内容主体并进行搜索和替换。这很脆弱,而且非常占用资源。如果您确实需要这样做,可以使用 mod_proxy_html
、mod_substitute
或 mod_sed
来完成此任务。
如果您使用 Microsoft IIS 作为 Web 服务器,ISAPI 重定向器提供了一种使用内置功能执行第一步的方法。您可以为像这样的简单前缀更改定义一个映射文件
# Add a context prefix to all requests ...
/=/myapp/
# ... or change some prefix ...
/oldapp/=/myapp/
然后将文件名放在注册表中的 rewrite_rule_file
条目或 isapi_redirect.properties
文件中。在 uriworkermap.properties
文件中,您仍然需要映射重写之前的 URL!
可以使用同一个文件进行更复杂的重写,但需要使用正则表达式。以波浪号 '~
' 开头的符号表示您正在使用正则表达式。
# Use a regular expression rewrite
~/oldapps([0-9]*)/=/newapps$1/
不支持步骤 2(重写重定向响应)或步骤 3(重写 cookie 路径)。
URL 编码
某些类型的问题是由使用编码的 URL 引发的(参见 百分比编码)。对于同一个位置,存在许多不同的 URL,它们是等效的。反向代理需要检查 URL 以应用其自身的身份验证规则并决定将请求发送到哪个后端(或是否应该自行处理它)。因此,请求 URL 首先会被规范化:百分比编码的字符会被解码,/./
会被替换为 /
,/XXX/../
会被替换为 /
,以及对 URL 进行类似的操作。之后,Web 服务器可能会应用重写规则以进一步以不明显的方式更改 URL。最后,没有更多的方法将生成的 URL 放入与用于原始 URL 的编码“类似”的编码中。
出于历史原因,mod_jk 和 ISAPI 插件在将生成的 URL 发送到后端之前对它进行编码的方式有几种选择。它们可以通过 JkOptions
(mod_jk)或 uri_select
(ISAPI)进行选择。所有这些历史编码都不推荐使用,因为它们要么具有负面的功能影响,要么会带来安全风险。从 1.2.24 版本开始的默认编码是 ForwardURIProxy
(mod_jk)或 proxy
(ISAPI),强烈建议保留默认设置并删除所有旧的显式设置。
请求属性
在使用 Apache HTTP Server 时,您还可以向您正在转发的任何请求添加更多属性。为此,请使用 JkEnvVar
指令(有关详细信息,请参见 Apache 参考 页面)。这些请求属性可以通过 request.getAttribute(attributeName) 在 Tomcat 端检索。请注意,通过 mod_jk 设置的属性名称不会列在 request.getAttributeNames() 中!