LaravelのURL Generatorの出力をCloudFront + ALB環境下でhttps化する

LaravelのURL Generatorを使ったリンク、つまりurl()、asset()、route()などのパスを生成するための関数で発行されるリンクが、特定のAWS環境下でhttpになってしまうことへの対応です。

LaravelをインストールしたEC2がCloudFrontやALB配下に入っていると、内部通信はhttpsではなくhttpで来ていることがほとんどかと思います。(全EC2をSSL通信させるという設定も可能ですが、あまり使わないと思います)
この環境下でURL Generatorを使うとhttp通信と勘違いして生成されるURLが全てhttpとなってしまいます。

結論としては、CloudFrontからのヘッダ情報を見てHTTPSかHTTPかを出し分けるMiddlewareを作成しました。
具体的な方法は最後に記載します。

では、実際試したことを見て行きましょう。

何も考えず検索結果をコピペしてみる

これ一番やっちゃダメなこととわかりつつ、こうすれば動くよというだけで中身までしっかり紹介しているサイトがほぼ無いのでいろいろ試行錯誤してみました。

URL::forceSchema(‘https’); を使う

まずは昔からよく聞く、すべてをhttpsとして取り扱う URL::forceSchema(‘https’); を使った方法です。
app/Providers/AppServiceProvider.php の boot に記載します。

public function boot()
{
    if (App::environment() === 'production') {
        URL::forceSchema('https');
    }
}

実行環境がproductionの場合に全てをhttps扱いにするというものです。
今回実は別のかたが作った環境の改修案件で、同様の記述がなぜかAppServiceProviderではなくroute定義ファイルの中に書かれていました。
動きそうだけど、route内でURL:: という記述でURL Generatorって動くんだっけ?ダメならエラーになるか…と思いつつ念のためAppServiceProviderへの追加を試してみましたが、こちらは動作しませんでした。
(一番乱暴ながら確実な方法な気がするのに、それはそれでなんで??という感じではあります)

TrustedProxyを使う

次に試したのが、TrustedProxyを使う方法。
こちらはLaravel 5.5以降であれば使える方法です。(下位バージョンの場合はモジュールのインストールを行う必要があります)

URL Generatorの動作としては、RequestのgetScheme()の部分で、isSecure()の値によってhttpとhttpsの出し分けが行われています。
そのisSecure()の結果にこのTrustedProxyが使われているようです。

詳細は公式ドキュメントに記述があります。

具体的には、 app/Http/Middleware/TrustProxies.php の変数に信頼できるホストとしてALBを追加するという方法です。

protected $proxies;

となっているところを

protected $proxies = '*';

と書き換えます。

今回の環境ではセキュリティグループにてアクセス元を絞っているため、*として全てを設定してしまっていますが、できればIPレンジを絞って設定した方が良いでしょう。
この設定は単体ではうまく動作してくれませんでしたが、必要な設定であることに間違いありません。
ということで動作確認後もそのまま残しておきました。

ちなみにその下にある

protected $headers = Request::HEADER_X_FORWARDED_ALL;

というのを書き換える方法を紹介しているサイトもありましたが、うまく動作しませんでした。

ここではHTTPSであることを示すヘッダのチェックしているのだと思います。
例えばALBからは X-Forwarded-Proto が来ますので、HEADER_X_FORWARDED_PROTOに書き換えるというサイトもありましたが、このALLの記述のままでも拾うことができていそうです。
つまり、CloudFront無しのALBからEC2上のLaravelという場合はここまでの設定で動きそうです。(確認していません)

うまく動作させることができてからこれでは動かなかった理由を考えると、この部分の書き方を工夫すれば期待通り動作させることができそうですが、今回は別のMiddlewareを作りKernelに組み込むという方法で動作させることができました。

期待どおりhttpsを返すまでの顛末

ということで、実際に動くようになった実装をご紹介しておきます。
参考にしたのは、LIGさんの「Amazon CloudFrontを経由したLaravelがHTTPSにならないとき」という記事です。

まずは前述のTrustedProxyの$proxiesにAWS内のローカルIP帯域または*を設定しておきます。

次にヘッダですが、CloudFrontでは X-Forwarded-xxxxx ではなく、CloudFront-Forwarded-Proto でHTTPSを送ってくるということで、参考サイトのとおりTrustCloudfrontProxiesというMiddlewareを作ってKernelに登録します。
TrustCloudfrontProxiesは X-Forwarded-Proto に CloudFront-Forwarded-Proto の値を設定するというMiddlewareで、元々あるTrustProxiesの後ろに入れます。

これで行けるだろう!と試してみるとそんなに甘くありませんでしたので、最終的にfixしたコードは最後に載せます。

さて、論理的にはこれで間違っていないはずです。
状況を確認すべく、トップページのControllerに受信したすべてのヘッダ情報を表示する記述を追加して確認します。
すべてのヘッダ情報を取得するにはphpのgetallheadersという関数を使用します。
phpのマニュアルページに全ての情報を表示するためのサンプルがありましたので、そのまま使いました。

すると、CloudFront-Forwarded-Proto なんてヘッダは飛んできていないようです。

AWSの管理画面に入り、CloudFrontの設定を確認します。
behaviorの設定でオリジンに送るヘッダ情報を選ぶところがあり、そこに CloudFront-Forwarded-Proto を追加しました。

CloudFrontの出力ヘッダ設定

早速動作確認です。
httpsきた!と思ったら、何かがおかしいのです。

URL Generatorが生成したURLには全て https://example.com:80 のようにポート番号が付加されています。
なかなか思うようには行きません。

先ほどのヘッダ確認の際に、X-Forwarded-Port に80がセットされており、この値を使っていそうということでTrustCloudfrontProxiesに CloudFront-Forwarded-Proto の値を見て X-Forwarded-Port に443をセットするような記述を入れてみました。
これで期待通りURL Generatorで正しいhttpsのURLが出力されるようになりました。

最終的なコード

それではお待たせいたしました。
以下、実際のコードをご紹介しておきます。

<?php
namespace App\Http\Middleware;

use Fideloper\Proxy\TrustProxies as Middleware;
use Illuminate\Http\Request;

class TrustProxies extends Middleware
{
    /**
     * The trusted proxies for this application.
     *
     * @var array|string
     */
     //protected $proxies;
     protected $proxies = '*';

    /**
     * The headers that should be used to detect proxies.
     *
     * @var int
     */
     protected $headers = Request::HEADER_X_FORWARDED_ALL;

    // protected $headers = Request::HEADER_X_FORWARDED_AWS_ELB;
    //AWS Elastic Load Balancingを使用している場合、$headersの値はRequest::HEADER_X_FORWARDED_AWS_ELBに設定する必要があり
    //Request::HEADER_X_FORWARDED_AWS_ELB
}
<?php
namespace App\Http\Middleware;

use Closure;

class TrustCloudfrontProxies
{
    public function handle($request, Closure $next)
    {
        if ($request->header('cloudfront-forwarded-proto')) {
            $headers = $request->headers;
            $headers->add(['x-forwarded-proto' => $headers->get('cloudfront-forwarded-proto')]);
            $headers->add(['x-forwarded-port' => ($headers->get('cloudfront-forwarded-proto') == 'https')?443:80]);
        }
        return $next($request);
    }
}
<?php
namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \App\Http\Middleware\TrustProxies::class,
        \App\Http\Middleware\TrustCloudfrontProxies::class,
        // (略)
    ];

    // (以下略)

LaravelのURL Generatorの出力をCloudFront + ALB環境下でhttps化するにコメントする

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です