在反向代理(ProxyPass)后面配置带有Mojolicious前缀的url



我正在寻找一种可靠的方法来配置Mojolicious在/app下的Apache反向代理后面运行,以便url_for('/foo')实际上返回/app/foo而不仅仅是/foo(否则所有链接都会断开)。

文档显示了/下所有内容的反向代理示例。但这不是我需要的,因为应用程序应该在/app下。
ProxyPass / http://localhost:8080/转换为ProxyPass /app http://localhost:8080/将导致问题,因为/app前缀将从应用程序生成的所有url中丢失。

文档也有一个关于重写的章节,其中有一个before_dispatch钩子的例子,它将获取请求url的第一部分并将其作为基础。这需要在Apache配置中添加前缀到ProxyPass url (ProxyPass /app http://localhost:8080/app/带尾斜杠),这似乎没有在该页上提到,但也许它不需要("移动第一部分和斜杠从路径到基本路径"),因为这是显而易见的。这使得可以调用http://localhost/app/page,它变成http://localhost:8080/app/page ('app'被钩子删除),其中url_for('/foo')将返回'/app/foo' (http://localhost/app/foo),因此链接将是正确的(在ProxyPass规则中没有尾斜杠,这将使/apppage/foo)。

但是,在本例中,url修改为总是在生产模式(if app->mode eq 'production')下进行的。所以直接调用后端服务器(http://localhost:8080/testpage)将不再工作,因为所有的url将被破坏。

所以我想,我会检查X-Forwarded-For头是否设置(由mod_proxy_http),这将始终为反向代理请求设置。由于Apache mod_proxy文档提到这个头可能已经在客户端请求中设置(并且最终包含多个值),我将首先从请求中删除它-因为客户端发送这个头不应该导致url基修改。

Apache VirtualHost配置:

# ProxyPreserveHost: Mojo::URL::to_abs() not 127.0.0.1
ProxyPreserveHost On
<Location "/app/">
    # ProxyPass: prefix pass-thru
    ProxyPass http://localhost:3000/app/
    # RequestHeader: must not be set externally
    RequestHeader unset X-Forwarded-For
</Location>

Hook in Mojolicious startup():

$self->hook('before_dispatch' => sub {
    my $c = shift;
    my $behind_proxy = !!$c->req->headers->header('X-Forwarded-Host');
    if ($behind_proxy) {
        push @{$c->req->url->base->path->trailing_slash(1)},
            shift @{$c->req->url->path->leading_slash(0)};
        $c->req->url->path->trailing_slash(0) # root 404
            unless @{$c->req->url->path->parts};
    }
});

这似乎工作…

问题:我的方法在"现实世界"中可靠地工作还是有缺陷?

编辑:
通过反向代理请求根地址(http://localhost:3000/app/)总是导致404错误。在这种情况下,我添加了两行来关闭尾斜杠。由于我在文档中找不到,可能有更好的方法。

需要在before_dispatch hook

中为每个请求url设置基路径
$app->hook(before_dispatch => sub {
  my $c = shift;
  $c->req->url->base->path('/app/');
});

的例子:

use Mojolicious::Lite;
app->hook(before_dispatch => sub {
  shift->req->url->base->path('/app/');
});

get '/' => sub {
  my $c = shift;
  $c->render(text => $c->url_for('test'));
};
get '/test/url' => sub { ... } => 'test';
app->start;

和结果:

$ curl 127.0.0.1:3000
/app/test/url

我现在正在回答我自己的问题,因为我得到了更多的建议(不仅仅是这里),他们会在他们的应用程序代码中放置硬编码前缀。很明显,手动为所有生成的url添加前缀并不是一个解决方案。想象一下在同一台服务器上部署同一个应用程序的两个实例,一个在/app1下,另一个在/app2下。我的问题中建议的代码的全部要点是,如果通过反向代理访问,应用程序可以工作并生成正确的url,而不会破坏直接发送到应用程序服务器的请求。开发人员可以运行Morbo,但硬编码前缀会破坏这一点。此外,我在问题中至少犯了一个错误,但似乎没有人注意到。

代码缺陷

我在问题中的例子中有太多的斜杠。根据Location块的定义方式,对/app的请求如果没有尾斜杠就会失败。不如这样写:

<Location "/app">
...

接下来,我写了我检查X-Forwarded-For头,但我实际上检查了X-Forwarded-Host。这不会是一个问题,如果我也清除头,但我清除X-Forwarded-For代替。有了这个尴尬的错误,安全机制就不起作用了,所以如果你在localhost:3000与应用服务器一起工作时设置了这个头,应用程序就会尝试修复被操纵的url,尽管它不应该这样做。

应该是:

RequestHeader unset X-Forwarded-Host

的例子:

ProxyPreserveHost On
<Location /app>
    ProxyPass http://localhost:3000/app
    RequestHeader unset X-Forwarded-Host
</Location>

只要应用程序到处使用相对url, ProxyPreserveHost指令就不是必需的。如果应用程序想要生成一个绝对url,例如url_for('/page')->to_abs,则应该启用ProxyPreserveHost,否则外部客户端将获得http://localhost:3000/app/page

反向代理检测

当我写这个问题时,我在Mojolicious文档中看到了before_dispatch钩子,正如问题中指出的那样,我想将它用于在/app下运行的应用程序。然而,我并不想破坏Morbo。该示例假设应用程序在反向代理后运行时处于生产模式($app->mode),但不是直接通过Morbo访问时,但我不想为每个其他请求更改模式。

这就是为什么我添加了一个条件来检查请求是否通过反向代理。由于此头仅由Apache(由mod_proxy_http模块)设置,而不是由Mojo::Server::Morbo设置,因此它可以用作反向代理检测。加上正确的指令来清除X-Forwarded-Host,我相信我的问题的答案是,这应该可靠地工作。

(尽管最后一部分不是严格必要的,只要直接访问应用服务器仅限于开发人员。)

<<h3>操作url/h3>

为了说明为什么我在Apache配置中的ProxyPass行中添加了/app前缀,我想指出这种方法操纵url以允许应用程序在给定的前缀下工作。还有一个问题,有人忘记在Apache配置中添加前缀,我写了一个答案来解释钩子的作用。

Morbo: localhost:3000
Apache reverse proxy: host.com/app or localhost/app
# Browser > Apache:
http://host.com/app
# Apache (ProxyPass http://localhost:3000/) > Mojolicious sees:
GET /
url_for '/test' = /test 
(or even //test if the hook pushes undef, see the other answer linked above)
# Apache (configured as described above) > Mojolicious sees:
GET /app
# Hook:
base = /app
request = /
url_for '/test' = /app/test 

通常,ProxyPass指令中的本地目标参数不会有前缀,它只是ProxyPass http://...:3000/之类的东西。在这种情况下,应用程序不知道前缀,这就是为什么所有生成的url和链接都是不完整的。

这种方法要求您让Apache将前缀传递给应用服务器。应用程序不知道前缀,因此它不知道如何处理对/app/page的请求。这就是钓钩的由来。它假设路径的第一层始终是前缀,因此它将/app/page转换为/page,并方便地将/app前缀附加到url基,这在生成url时使用,确保指向/test的链接实际上指向/app/test

显然,对于任何直接发送到Morbo的请求,都不应该进行此修改。

或者,反向代理可以设置自定义请求头,然后由钩子使用它来生成工作url。Mojolicious::Plugin::RequestBase模块就是这样工作的。它希望您在X-Request-Base报头中定义前缀,而不是在url:

中定义前缀。
RequestHeader set X-Request-Base "/app"
在这种情况下,应用程序应该只接收相对于该前缀的url请求:
ProxyPass http://localhost:3000/

该模块真正做的就是挑选标题并使用它作为url base:

$c->req->url->base($url); # url = X-Request-Base = /app

的例子:

<Location /app>
    ProxyPass http://localhost:3000
    RequestHeader set X-Request-Base "/app"
</Location>

这是一个很好的简单的解决方案。注意,在这种情况下,/app前缀出现了两次。当然,该模块实现的钩子只有在设置了X-Request-Base标头后才会工作,就像上面显示的钩子在没有设置X-Forwarded-Host标头时什么也不做一样。

您应该将应用程序挂载在所需的路径下。

在你的startup你应该做:

$r =  $app->routes;
$r =  $r->any( '/api' )->partial( 1 );
$r->get( '/test' );

你不应该特别配置你的apache。当GET /api/test到来时,你的应用程序将获得/api/test路由。该路由部分匹配/api,其余路由/test将分配给->stash->{ path }

所以休息路线将根据/test (source)

进行检查

最新更新