你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

在反向代理及其后端 Web 应用程序之间保留原始 HTTP 主机名

Azure API 管理
Azure 应用服务
Azure 应用程序网关
Azure Front Door
Azure Spring Apps

在 Web 应用程序的前面使用反向代理时,我们建议保留原始 HTTP 主机名。 在反向代理中使用与提供给后端应用程序服务器的主机名不同的主机名可能会导致 Cookie 或重定向 URL 无法正常工作。 例如,会话状态可能会丢失、身份验证可能失败,或者无意中将后端 URL 透露给最终用户。 可以通过保留初始请求的主机名来避免这些问题,以便应用程序服务器看到与 Web 浏览器相同的域。

本指南尤其适用于托管在平台即服务 (PaaS) 产品(例如 Azure 应用服务Azure Spring Apps)中的应用程序。 本文提供适用于 Azure 应用程序网关Azure Front DoorAzure API 管理的具体实施指南,它们都是常用的反向代理服务。

注意

Web API 一般对主机名不匹配造成的问题不太敏感。 它们通常不依赖于 Cookie,除非你使用 Cookie 来保护单页应用与其后端 API 之间的通信,例如,采用一种称为前端的后端模式。 Web API 通常不会将绝对 URL 返回给自身,但在某些 API 样式(例如 ODataHATEOAS)中除外。 如果 API 实现依赖于 Cookie 或生成绝对 URL,则本文中提供的指南适用。

如果你需要端到端的 TLS/SSL(反向代理与后端服务之间的连接使用 HTTPS),则后端服务还需要原始主机名的匹配 TLS 证书。 在部署和续订证书时,此项要求会增大操作复杂性,但许多 PaaS 服务提供完全托管的免费 TLS 证书。

上下文

HTTP 请求的主机

在许多情况下,请求管道中的应用程序服务器或某个组件需要拥有由浏览器用来访问该服务器或组件的 Internet 域名。 这就是请求的主机。 它可以是一个 IP 地址,但通常是类似于 contoso.com 的名称(浏览器随后使用 DNS 将其解析为 IP 地址)。 主机值通常由请求 URI 的 host 组成部分确定,浏览器会将此部分作为 Host HTTP 标头发送给应用程序。

重要

切勿在安全机制中使用主机的值。 该值由浏览器或其他某个用户代理提供,最终用户可以轻易操控它。

在某些情况下,尤其是当请求链中存在 HTTP 反向代理时,原始主机头在进入应用程序服务器之前可能会更改。 反向代理关闭客户端网络会话并与后端建立新的连接。 在此新会话中,它可以沿用客户端会话的原始主机名,也可以设置一个新的主机名。 对于后一种情况,代理通常仍会在其他 HTTP 标头(例如 ForwardedX-Forwarded-Host)中发送原始主机值。 此值让应用程序能够确定原始主机名,但前提是它们已编写为读取这些标头。

Web 平台为何使用主机名

多租户 PaaS 服务通常需要一个已注册并验证的主机名才能将传入请求路由到租户的适当后端服务器。 这是因为,通常有一个共享的负载均衡器池接受所有租户的传入请求。 租户通常使用传入的主机名来查找客户租户的正确后端。

为方便操作,这些平台通常会提供一个默认域,该域已预配置为将流量路由到部署的实例。 对于应用服务,此默认域为 azurewebsites.net。 创建的每个 Web 应用具有自身的子域,例如 contoso.azurewebsites.net。 类似地,Spring Apps 的默认域为 azuremicroservices.io,API 管理的默认域为 azure-api.net

对于生产部署,请不要使用这些默认域, 而是提供自己的域,以便与组织或应用程序的品牌保持一致。 例如,contoso.com 在幕后可能解析为应用服务上的 contoso.azurewebsites.net Web 应用,但访问该网站的最终用户不应看到此域。 但是,必须将此自定义 contoso.com 主机名注册到 PaaS 服务,使平台可以识别应该响应请求的后端服务器。

Diagram that illustrates host-based routing in App Service.

应用程序为何使用主机名

应用程序服务器需要主机名的两个常见原因是构造绝对 URL 以及为特定域发出 Cookie。 例如,如果应用程序代码需要:

  • 在其 HTTP 响应中返回绝对 URL 而不是相对 URL(不过在一般情况下,网站倾向于尽量呈现相对链接)。
  • 生成要在其 HTTP 响应之外使用的 URL(例如,用于通过电子邮件将网站链接发送给用户),且不能使用相对 URL。
  • 为外部服务生成绝对重定向 URL。 例如,向 Microsoft Entra ID 等身份验证服务指示成功完成身份验证后应将用户返回到的位置。
  • 根据 Cookie 的 Domain 属性中的定义,发出仅限于特定主机的 HTTP Cookie。

可以通过将预期的主机名添加到应用程序的配置,并在请求中使用静态定义的该值而不是传入主机名,来满足所有这些要求。 但是,此方法会使应用程序开发和部署变得复杂。 此外,应用程序的单个安装可为多个主机提供服务。 例如,单个 Web 应用可用于多个应用程序租户,这些租户都有自身的唯一主机名(例如 tenant1.contoso.comtenant2.contoso.com)。

有时,传入的主机名由应用程序代码外部的组件使用,或者在你无法完全控制的应用程序服务器上的中间件中使用。 下面是一些示例:

  • 在应用服务中,可为 Web 应用强制实施 HTTPS。 这会导致任何不安全的 HTTP 请求重定向到 HTTPS。 在这种情况下,传入的主机名用于为 HTTP 重定向的 Location 标头生成绝对 URL。
  • Spring Apps 使用类似的功能来强制实施 HTTPS。 它还使用传入的主机来生成 HTTPS URL。
  • 应用服务具有一个 ARR 相关性设置,该设置可以启用粘性会话,使得来自同一浏览器实例的请求始终由同一个后端服务器提供服务。 此操作由应用服务前端执行,它们会在 HTTP 响应中添加一个 Cookie。 该 Cookie 的 Domain 设置为传入的主机。
  • 应用服务提供身份验证和授权功能,使用户能够轻松登录以及在 API 中访问数据。

为何你有时忍不住替代主机名

假设你在应用服务中创建了一个默认域为 contoso.azurewebsites.net 的 Web 应用程序。 (或者它是在另一个服务中创建的,例如 Spring Apps。)你尚未在应用服务上配置自定义域。 若要在此应用程序的前面放置应用程序网关(或任何类似服务)等反向代理,请将 contoso.com 的 DNS 记录设置成解析为应用程序网关的 IP 地址。 因此,它会从浏览器接收对 contoso.com 的请求,并配置为将该请求转发到 contoso.azurewebsites.net 解析为的 IP 地址:这是请求的主机的最终后端服务。 但在这种情况下,应用服务无法识别 contoso.com 自定义域,并会拒绝此主机名的所有传入请求。 它无法确定要将请求路由到何处。

使此配置有效的简单方法似乎是替代或重写应用程序网关中 HTTP 请求的 Host 标头,并将其设置为 contoso.azurewebsites.net 值。 如果你这样做,来自应用程序网关的传出请求会使原始请求看起来确实是针对 contoso.azurewebsites.net 而不是 contoso.com

Diagram that illustrates a configuration with the host name overridden.

此时,应用服务确实可以识别主机名,并且会接受请求而不要求配置自定义域名。 事实上,在应用程序网关中可以轻松将主机头替代为后端池的主机。 这甚至是 Azure Front Door 的默认做法

但是,如果应用看不到原始主机名,这种解决方法可能会导致各种问题。

潜在问题

绝对 URL 不正确

如果未保留原始主机名并且应用程序服务器使用传入的主机名生成绝对 URL,则可能会向最终用户透露后端域。 这些绝对 URL 可以由应用程序代码生成,或者如前所述,由平台功能(例如应用服务和 Spring Apps 中的 HTTP 到 HTTPS 重定向支持)生成。 下图演示了此问题:

Diagram that illustrates the problem of incorrect absolute URLs.

  1. 浏览器向反向代理发送 contoso.com 请求。
  2. 在对后端 Web 应用程序(或其他服务的类似默认域)发出的请求中,反向代理将主机名重写为 contoso.azurewebsites.net
  3. 应用程序根据传入的 contoso.azurewebsites.net 主机名生成绝对 URL,例如 https://contoso.azurewebsites.net/
  4. 浏览器遵循此 URL,该 URL 直接转到后端服务,而不是返回到 contoso.com 上的反向代理。

在反向代理同时充当 Web 应用程序防火墙的常见情况下,这甚至可能会带来安全风险。 用户会收到一个直接转到后端应用程序并绕过反向代理的 URL。

重要

由于存在这种安全风险,需要确保后端 Web 应用程序只会直接接受来自反向代理的网络流量(例如,使用应用服务中的访问限制)。 这样,即使生成了错误的绝对 URL,也至少能保证该 URL 不起作用,恶意用户无法使用它来绕过防火墙。

重定向 URL 不正确

生成绝对重定向 URL 时,会发生上一方案的常见且更具体的情况。 当你使用基于浏览器的标识协议(例如 OpenID Connect、OAuth 2.0 或 SAML 2.0)时,Microsoft Entra ID 等标识服务需要这些 URL。 这些重定向 URL 可以由应用程序服务器或中间件本身生成,或者如前所述,由应用服务身份验证和授权功能等平台功能生成。 下图演示了此问题:

Diagram that illustrates the problem of incorrect redirect URLs.

  1. 浏览器向反向代理发送 contoso.com 请求。
  2. 在对后端 Web 应用程序(或其他服务的类似默认域)发出的请求中,反向代理将主机名重写为 contoso.azurewebsites.net
  3. 应用程序根据传入的 contoso.azurewebsites.net 主机名生成绝对重定向 URL,例如 https://contoso.azurewebsites.net/
  4. 浏览器转到标识提供者以便对用户进行身份验证。 请求包含生成的重定向 URL,指示成功完成身份验证后要将用户返回到何处。
  5. 标识提供者通常要求提前注册重定向 URL,因此,此时标识提供者应拒绝请求,因为提供的重定向 URL 未注册。 (不应该使用它。)但是,如果出于某种原因注册了重定向 URL,则标识提供者会将浏览器重定向到身份验证请求中指定的重定向 URL。 在此情况下,URL 为 https://contoso.azurewebsites.net/
  6. 浏览器遵循此 URL,该 URL 直接转到后端服务,而不是返回到反向代理。

Cookie 损坏

当应用程序服务器发出 Cookie 并使用传入的主机名构造 Cookie 的 Domain 属性时,主机名不匹配也可能导致问题。 Domain 属性确保 Cookie 仅用于该特定域。 这些 Cookie 可以由应用程序代码生成,或者如前所述,由应用服务 ARR 相关性设置等平台功能生成。 下图演示了此问题:

Diagram that illustrates an incorrect cookie domain.

  1. 浏览器向反向代理发送 contoso.com 请求。
  2. 在对后端 Web 应用程序(或其他服务的类似默认域)发出的请求中,反向代理将主机名重写为 contoso.azurewebsites.net
  3. 应用程序根据传入的 contoso.azurewebsites.net 主机名生成一个使用域的 Cookie。 浏览器存储此特定域(而不是用户实际使用的 contoso.com 域)的 Cookie。
  4. 浏览器不会在 contoso.com 的任何后续请求中包含 Cookie,因为 Cookie 的 contoso.azurewebsites.net 域与请求的域不匹配。 应用程序不会接收它先前发出的 Cookie。 因此,用户可能会丢失 Cookie 中应有的状态,或者 ARR 相关性等功能不起作用。 遗憾的是,这些问题都不会生成错误或者直接对最终用户可见。 这使得他们难以进行排除故障。

适用于常用 Azure 服务的实施指南

为避免此处所述的潜在问题,我们建议在反向代理与后端应用程序服务器之间的调用中保留原始主机名:

Diagram that shows a configuration in which the host name is preserved.

后端配置

许多 Web 托管平台要求显式配置允许的传入主机名。 以下部分介绍如何为最常用的 Azure 服务实施此配置。 其他平台通常会提供类似的方法来配置自定义域。

如果在应用服务中托管 Web 应用程序,则可以将自定义域名附加到 Web 应用,并避免对后端使用默认的 azurewebsites.net 主机名。 将自定义域附加到 Web 应用时无需更改 DNS 解析:可以使用 TXT 记录验证域,而不会影响常规的 CNAMEA 记录。 (这些记录仍将解析为反向代理的 IP 地址。)如果需要端到端的 TLS/SSL,可以从密钥保管库导入现有证书或者为自定义域使用应用服务证书。 (请注意,在这种情况下不能使用免费的应用服务托管证书,因为它要求域的 DNS 记录直接解析为应用服务而不是反向代理。)

同样,如果使用的是 Spring Apps,则可为应用使用自定义域,以避免使用 azuremicroservices.io 主机名。 如果需要端到端的 TLS/SSL,可以导入现有的或自签名的证书。

如果 API 管理的前面有一个反向代理(API 管理本身也充当反向代理),你可以在 API 管理实例上配置一个自定义域以避免使用 azure-api.net 主机名。 如果需要端到端的 TLS/SSL,可以导入现有证书或免费的托管证书。 但是,如前所述,API 对主机名不匹配造成的问题不太敏感,因此此配置可能不太重要。

如果在其他平台上托管应用程序(例如在 Kubernetes 上或直接在虚拟机上),则没有内置功能依赖于传入的主机名。 你需要负责主机名在应用程序服务器本身中的使用方式。 有关保留主机名的建议通常仍然适用于应用程序中依赖该主机名的任何组件,除非专门让应用程序感知反向代理并遵循 forwardedX-Forwarded-Host 之类的标头。

反向代理配置

在反向代理中定义后端时,仍可以使用后端服务的默认域,例如 https://contoso.azurewebsites.net/。 反向代理使用此 URL 来解析后端服务的正确 IP 地址。 如果使用平台的默认域,则始终可以保证 IP 地址正确。 通常不能使用面向公众的域,例如 contoso.com,因为它应该解析为反向代理本身的 IP 地址。 (除非使用更高级的 DNS 解析技术,例如水平分割 DNS)。

重要

如果反向代理与最终后端之间存在 Azure 防火墙高级版之类下一代防火墙,则可能需要使用水平分割 DNS。 此类防火墙可以显式检查 HTTP Host 标头是否解析为目标 IP 地址。 在这种情况下,浏览器使用的原始主机名在从公共 Internet 访问时应解析为反向代理的 IP 地址。 但是,从防火墙的角度看,该主机名应该解析为最终后端服务的 IP 地址。 有关详细信息,请参阅使用 Azure 防火墙和应用程序网关实现 Web 应用程序的零信任网络

大多数反向代理允许配置要向后端服务传递哪个主机名。 以下信息解释了如何确保对最常用的 Azure 服务使用传入请求的原始主机名。

注意

在所有情况下,还可以选择将主机名替代为显式定义的自定义域,而不是从传入的请求中获取主机名。 如果应用程序仅使用单个域,这种方法可能有效。 如果同一个应用程序部署接受来自多个域的请求(例如,在多租户方案中),则不能以静态方式定义单个域。 应该从传入请求中获取主机名(同样,除非应用程序已显式编写为考虑其他 HTTP 标头)。 因此,一般建议是完全不要替代主机名。 将未修改的传入主机名传递给后端。

应用程序网关

如果使用应用程序网关作为反向代理,则可以通过在后端 HTTP 设置中禁用“替代为新主机名”来确保保留原始主机名。 这样做会同时禁用“从后端地址中选取主机名”和“替代为特定域名”。 (这两个设置都会替代主机名。)在应用程序网关的 Azure 资源管理器属性中,此配置对应于将 hostName 属性设置为 null,以及将 pickHostNameFromBackendAddress 设置为 false

由于运行状况探测是在传入请求的上下文之外发送的,因此它们无法动态确定正确的主机名。 必须创建自定义运行状况探测,禁用“从后端 HTTP 设置中选取主机名”,并显式指定主机名。 对于此主机名,还应使用适当的自定义域以保持一致。 (但是,在此处可以使用托管平台的默认域,因为运行状况探测会忽略响应中错误的 Cookie 或重定向 URL。)

Azure Front Door

如果使用 Azure Front Door,则可以通过在后端池定义中将后端主机头留空来避免替代主机名。 在后端池的资源管理器定义中,此配置对应于将 backendHostHeader 设置为 null

如果使用 Azure Front Door 标准版或高级版,则可以通过在源定义中将源主机头留空来保留主机名。 在源的资源管理器定义中,此配置对应于将 originHostHeader 设置为 null

API 管理

默认情况下,API 管理会将发送到后端的主机名替代为 API 的 Web 服务 URL 的 host 组成部分(对应于 API 的资源管理器定义serviceUrl 值)。

可以通过添加一个inbound设置 HTTP 标头策略来强制 API 管理改用传入请求的主机名,如下所示:

<inbound>
  <base />
  <set-header name="Host" exists-action="override">
    <value>@(context.Request.OriginalUrl.Host)</value>
  </set-header>
</inbound>

但是,如前所述,API 对主机名不匹配造成的问题不太敏感,因此此配置可能不太重要。

后续步骤