我正在寻找一种可靠的方法来配置在/ app下的Apache反向代理后面运行Mojolicious,这样url_for('/foo')
实际上返回/app/foo
而不仅仅是/foo
(否则所有链接都会被破坏)。
documentation显示了/下所有内容的反向代理示例。但这不是我需要的,因为应用程序应该在/ app下。
将ProxyPass / http://localhost:8080/
转换为ProxyPass /app http://localhost:8080/
将导致问题,因为应用程序生成的所有URL中都缺少/app
前缀。
该文档还有一个section on rewriting,它有一个before_dispatch钩子的例子,它将获取请求url的第一部分并将其用作基础。这需要在Apache配置中将前缀附加到ProxyPass网址(带斜杠的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 documentation提到这个标头可能已经在客户端请求中设置(并且最终包含多个值),我首先将其从请求中删除 - 因为发送此标头的客户端不应该导致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>
在Mojolicious启动中的钩子():
$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挂钩中为每个请求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
。
当我写这个问题时,我在before_dispatch
中看到了Mojolicious documentation钩子,正如问题中指出的那样,我想将它用于在/app
下运行的应用程序。但是,我不想打破摩博。该示例假定应用程序在反向代理后面运行时处于生产模式($app->mode
),但在直接通过Morbo访问时不会,但我不想为每个其他请求更改模式。
这就是为什么我添加了一个条件来检查请求是否来自反向代理。由于此标头仅由Apache(由mod_proxy_http module)设置而不是由Mojo::Server::Morbo
设置,因此它可以用作反向代理检测。连同正确的指令清除X-Forwarded-Host
,我相信我的问题的答案是肯定的,那应该可靠地运作。
(尽管只要对应用服务器的直接访问仅限于开发人员,最后一部分就不是必需的。)
为了说明为什么我在Apache配置中为/app
行添加了ProxyPass
前缀,我想指出这种方法操纵url以允许应用程序在给定前缀下工作。有人忘记在Apache配置中添加前缀并且我写了一个another question的answer explaining what the hook does。
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 base,这在生成url时使用,确保/test
的链接实际上指向/app/test
。
显然,不应对直接发送给Morbo的任何请求进行此修改。
或者,可以通过反向代理设置自定义请求标头,然后由钩子使用它来生成工作URL。 Mojolicious::Plugin::RequestBase module以这种方式运作。它希望您在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
前缀出现两次。当然,如果设置了hook implemented by that module标头,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)进行核对