我正在寻找一种可靠的方法来配置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)