反向代理操作指南

简介

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”。
请记住:通常您不需要设置这些。在运行 mod_jk 的 Web 服务器知道正确数据的所有情况下,AJP 都会自动处理。

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