Apache HTTP Server 模块 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 或 IIS 使用 1.2.41 版本并结合至少 6.0.42、7.0.55 或 8.0.11 版本的 Tomcat 时,此功能可用。对于旧版本或使用 NSAPI 重定向器时,getLocalAddr()
将错误地返回与getLocalName()
相同的结果。作为一种变通方法,您可以通过设置JkEnvVar SERVER_ADDR
来转发本地 IP 地址,然后使用request.getAttribute("SERVER_ADDR")
代替getLocalAddr()
,或者使用过滤器包装请求并用request.getAttribute("SERVER_ADDR")
覆盖getLocalAddr()
。 - 本地端口:
getLocalPort()
。这与getServerPort()
相同,除非请求中包含Host
标头。在这种情况下,如果该标头包含明确的端口,则服务器端口取自该标头,否则等于所用方案的默认端口。 - 客户端地址:
getRemoteAddr()
- 客户端端口:
getRemotePort()
。远程端口最初不受支持。当 Apache 或 IIS 使用 1.2.32 版本并结合至少 5.5.28、6.0.20 或 7.0.0 版本的 Tomcat 时,此功能可用。对于旧版本或使用 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
时,Apache HTTP Server 才会提供并由 mod_jk 转发以下额外的 SSL 相关数据。对于证书信息,您还需要设置 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-Forwarded-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 Server 直接设置。如果您想更改数据,但又不想对其他模块的行为产生负面影响,可以将 mod_jk 使用的所有变量的名称更改为私有名称。有关详细信息,请参阅 Apache 参考页面。
所有非 SSL 相关的变量仅在 1.2.27 版本中引入。
此外,还有两个特殊的快捷方式可以影响转发的客户端 IP 地址。使用 JkOptions ForwardLocalAddress
,您可以将 Web 服务器的本地 IP 地址作为客户端 IP 地址转发。这在例如使用 Tomcat 远程地址阀门仅允许来自注册的 Apache HTTP Server 的连接时非常有用。使用 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 上下文,例如因为每个主机只能有一个应用程序作为根上下文。
在反向代理中更改 URL 的过程很繁琐,因为应用程序通常会生成自引用 URL,其中包含您试图向外部世界隐藏的路径组件。尽管如此,如果您绝对需要这样做,以下是步骤。
情况 A:您需要让应用程序在简单的 URL 下可用,但如果用户使用更复杂的 URL 进行访问,只要他们不必手动输入,那也没问题。这是简单的情况,如果这满足您的需求,那么您很幸运。为 Apache HTTP Server 使用一个简单的 RedirectMatch
RedirectMatch ^/$ http://www.mycompany.com/myapp/
您的应用程序将在 http://www.mycompany.com/
下可用,并且每个访问者都将立即被重定向到真实的 URL http://www.mycompany.com/myapp/
情况 B:您需要隐藏所有发往应用程序的请求的路径组件。以下是您想要隐藏第一个路径组件 /myapp
的情况的解决方案。更复杂的操作留给读者自行练习。首先是 Apache HTTP Server 的解决方案
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/
然后将文件名放入注册表或您的 isapi_redirect.properties
文件中的 rewrite_rule_file
条目。在您的 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 参考页面)。此类请求属性可以在 Tomcat 端通过 request.getAttribute(attributeName) 检索。请注意,通过 mod_jk 设置的属性名称不会列在 request.getAttributeNames() 中!