如何让 HWI OAuth Bundle 在反向代理后面的容器化应用程序中表现良好?

How do I have HWI OAuth Bundle behave well in a containerized application behind a reverse proxy?

上下文

多年来,我一直运行在 Symfony 3.x 中使用内部网管理面板。用户使用 google oauth 登录,系统检查电子邮件是否与 lookup-list 中经过验证的电子邮件匹配。 oauth 客户端处理是通过“HWI OAuth Bundle”完成的。

为了开始以一种干净的方式将此管理面板迁移到 SF4,然后再迁移到 SF5,我们已经开始将我们的单体应用分解为微服务 运行宁 docker。

移动到 docker 反向代理后面

今天我们将此管理面板移至 docker。然后我们让 public apache2 对 docker 运行 管理面板执行 ProxyPass。让我们想象 docker 运行s 在 http://1.2.3.4:7540 让我们假设 public 地址是 https://admin-europe.example.com

发生的事情是 symfony 应用程序有一个相对 URL,作为在 routing.yml 中配置的路由 google_login 和在 security.yml 中定义的服务配置:

路由:

# Required by the HWI OAuth Bundle.
hwi_oauth_redirect:
    resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
    prefix:   /connect

hwi_oauth_connect:
    resource: "@HWIOAuthBundle/Resources/config/routing/connect.xml"
    prefix:   /connect

hwi_oauth_login:
    resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"
    prefix:   /login

# HWI OAuth Bundle route needed for each resource provider.
google_login:
    path: /login/check-google

logout:
    path: /logout

安全性:

firewalls:
    # disables authentication for assets and the profiler, adapt it according to your needs
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false

    secured_area:
        anonymous: true
        logout:
            path:   /logout
            target: /
            handlers: [ admin.security.logout.handler ]
        oauth:
            resource_owners:
                google:        "/login/check-google"
            login_path:        /
            use_forward:       false
            failure_path:      /

            oauth_user_provider:
                service: admin.user.provider

因此,当应用程序未 docker 化时,它 运行 正确,因为请求成为 google 的“重定向路由”的路由是 https://admin-europe.example.com/login/check-google

尽管如此,现在它在 docker 内部,当 HWI 包正在构建要发送到 google 的数据块时,它请求将此 http://1.2.3.4:7540/login/check-google 授权为“重定向” URI”,但当然不应该。当然重定向 URI 应该继续是 https://admin-europe.example.com/login/check-google.

我自然会收到此错误消息:

反向代理

我们已经在反向代理 ProxyPassReverse 中拥有了 ProxyPassReverse 事实上,完全相同的配置已经与我们已经成功移动的另一个微服务一起工作了 hassle-free 一个多月(但是那个服务不需要身份验证,是一个 public 站点)。

这是自然的,因为 ProxyPassReverse 将处理 http 数据,但是 google-oauth info-block 不由 ProxyPassReverse 处理,因为它是自然的。

问题

这里的问题是验证此地址(将域别名放入私有 IP 地址等)

这里的问题是如何从 docker 内部生成“正确的 public URL”,而无需在函数中为容器内容创建 hard-dependency它将要 运行 的环境。这样做会是 anti-pattern.

探索解决方案

当然,“简单”的解决方案是在容器内“硬编码”“外部路由”。

但这有一个缺陷。如果我还希望从 https://admin-asia.example.com/ 访问相同的 docker(请注意 -asia 而不是 -europe),我将 运行 放入问题是亚洲用户将被重定向到欧洲路线。这只是一个例子,不要关心具体的 europe-asia 事情......重点是 container 应该 not注意周围的建筑。或者至少,有意识地“交互”,但绝对不要在容器内“硬编码”依赖于环境的东西。

即:忘记 -europe 和 -asia 的事情。假设访问权限是 admin-1111。如果有一天我希望它可以作为 admin-2222 访问,那么我必须“重新编译”和“重新部署”容器是没有意义的。

临时解决方案

我认为将 rounting.yml 中的路由和 security.yml 中的配置指向“参数”(在 3.x 中 parameters.yml) 然后在更新到 SF4 时将其移动到环境变量中,但我不确定 symfony 的缓存编译器如何处理没有值但“动态变化”的路由。

然后在容器启动时传递redirecion的值。这只能部分解决问题:所有容器都将绑定到在启动时设置的重定向路由,但它仍然不会解决通过访问同一容器实例的情况不同的名称因此需要多个重定向路由。相反,当 运行ning non-dockerized 工作时,它只需要“主机名”来在 relative-path 定义上构建绝对路径。

目前调查

访问时,浏览器显示我要去

https://accounts.google.com/o/oauth2/auth
?response_type=code
&client_id=111111111111-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.apps.googleusercontent.com
&scope=email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fplus.profile.emails.read+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fplus.login
&redirect_uri=http%3A%2F%2Fmy.nice.domain.example.com%3A7040%2Fapp_dev.php%2Flogin%2Fcheck-google

在这里我们看到 redirect_uri 参数是我们将 return 暂时将控制权交给 google 的地方。

所以需要有人来构建这个 URL。

我在源代码中寻找“redirect_uri”,我发现涉及的 class 是 GoogleResourceOwner 扩展 GenericOAuth2ResourceOwner.

两个 classes 似乎都属于 根据通过 $redirectUri 作为字符串的测试,该字符串需要由来电者。

涉及的方法是public function getAuthorizationUrl($redirectUri, array $extraParameters = array())。这会接收重定向 URI 并使用 redurect UR 构建 auth URI编码为参数。

那么,getAuthorizationUrl() 的 consumers/clients 是谁?

我只在函数 public function getAuthorizationUrl(Request $request, $name, $redirectUrl = null, array $extraParameters = array())

中的一行 return $resourceOwner->getAuthorizationUrl($redirectUrl, $extraParameters); 中找到了一个客户端用法 OAuthUtils

我看到这个 OAuthUtils 主要充当 Symfony Request 和 OAuth 域模型之间的适配器。在此方法中,我们主要找到创建 $redirectUri.

的代码

对我来说最干净的解决方案是创建一个继承自 OAuthUtils 的 child class OAuthUtilsBehindProxy,覆盖方法 getAuthorizationUrl() 并让它解释请求的 X-FORWARDED-* headers,然后让依赖项注入自动连接我的 class 在任何使用 OAuthUtils 的地方 希望 没有人做一个 new OAuthUtils 并且这个 class 的每个用户都将它传递给构造函数。

这将是干净的并且可以工作。

但坦率地说,这对我来说似乎有点矫枉过正。我很确定在我之前有人已经将需要 Google OAuth 的应用程序放在反向代理后面,我想知道是否有一个“配置选项”我丢失了或者我真的必须 re-code 所有这些并通过 D.I.

注入

所以,问题

当运行在反向代理后面的docker容器中关于如何为google-oauth服务?

有什么方法可以告诉 HWI 包或 symfony 在 X-FORWARDED-* headers“如果可用”的功能中添加“full-host”前缀?这将使 docker 图像“固定”,并且 运行 在“任何”环境中。

根本原因是 Symfony 从相对路径或路由名称生成 full-addresses 的方式。

调查如下:

  • 方法HWI/OAuthUtils::getAuthorizationUrl()是生成OAUth auth URI并使用方法Symfony/HttpUtils::generateUri()获取redirect_to回调的绝对URI的方法在 Auth URI 中进行编码。

  • 方法 Symfony/HttpUtils::generateUri() 生成一个绝对 URI(在我们的例子中是回调),为此,该方法处理 3 种一般情况:

    • 参数已经是绝对URI(return为未经进一步处理的参数)
    • 参数是相对的URL(函数调用Requestclass构建proto+host+port+project-path前缀添加到相对URI)
    • 参数为路由名称(函数调用Routerclass构建绝对URI)

在我的示例中,我在 security.yml 中配置了一个亲属 URL (google: "/login/check-google"),因此 HttpUtils 委托给了 Request class.

查看 Request class 的来源,我们观察到:

  • Request class 可以使用代理 headers 构建绝对 class.
  • 但出于安全考虑,默认情况下 symfony 不相信代理的存在仅仅因为其中有 X-FORWARDED-* headers。
  • 确实更安全也更灵活。
  • 有 2 个安全级别:
    • 我们需要在某个地方告诉 Request class 作为访问应用程序代理的受信任 IP 列表是什么。
    • 我们需要在其他地方告诉 Request class 哪些特定代理 headers 是可信的,哪些 headers 不是,即使它支持不同的标准 headers(RFC headers、non-RFC apache headers 等)

这里说的https://symfony.com/blog/fixing-the-trusted-proxies-configuration-for-symfony-3-3是需要在front-controller中通过调用静态方法Request::setTrustedProxies();

配置可信代理

所以在 front-controller 中添加这两行,一个是杀死非 nee4ded headers 另一个是代理的 IP 范围,解决了问题:

# app.php
<?php

use Symfony\Component\HttpFoundation\Request;

$loader = require __DIR__.'/../app/autoload.php';
include_once __DIR__.'/../var/bootstrap.php.cache';

$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();

Request::setTrustedHeaderName( Request::HEADER_FORWARDED, null );   # <-- Kill unneeded header.
Request::setTrustedProxies( [ '192.168.104.0/24', '10.0.0.0/8' ] ); # <-- Trust any proxy that lives in any of those two private nets.

$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

有了这个变化:

    如果通过代理调用,
  1. Symfony Request 能够从相对地址构建正确的 public 绝对地址,方法是从 HTTP_X_FORWARDED_HOSTHTTP_X_FORWARDED_PORT 中扣除主机而不是HTTP_HOSTSERVER_PORT.
  2. Symfony HttpUtils 也,因为它委托给 Request
  3. HWI 反过来能够构建正确的绝对回调 redirect_to
  4. HWI 可以设置在 AuthUri 中编码的正确回调。
  5. 考虑到代理效应,包含正确绝对 URI 的 AuthURI 被发送到 google。
  6. Google 将“public URI”视为在 google 配置中注册的 URI。
  7. 工作流完成,登录过程可以成功结束。