反向代理操作指南

简介

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()
以下其他与 SSL 相关的数据将由 Apache HTTP Server 提供,并仅在您设置 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”。
请记住:通常您不需要设置这些。AJP 会自动处理所有 Web 服务器运行 mod_jk 了解正确数据的情况。

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_htmlmod_substitutemod_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() 中!