Laravel5で.htaccessを使用せず常時SSL化対応する方法

### はじめに

普通、全ページをSSL化(https)にするなら.htaccessにこんな風に書きますよね。

```apache
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
```

静的に作ってるページしかないならこれだけで全部httpsにリダイレクトするので、なんだかよさそうな気がします。
しかし、この書き方はformが絡むと結構深刻な問題を引き起こします。

Laravelで簡単なお問い合わせフォームを作ったとして、次のような書き方をしていた場合

```php
{{ Form::open(['url' => route('inquiry.store'), 'method' => 'POST']) }}
	{{ csrf_token() }}
	{{ Form::text('name', NULL) }}
	{{ Form::submit('submit') }}
{{ Form::close() }}
```

実際には次のようなHTMLを出力します。

```html
<form method="POST" action="http://www.example.com/inquiry/store">
	<input name="_token" type="hidden" value="*******">
	<input name="name" type="text" value=""></td>
	<input type="submit" value="submit">
</form>
```

### .htaccessで記述した場合の問題点

注目すべきはaction属性の値。httpになっています。
Laravelのrouteやurlヘルパーの書き方は標準だと、httpから始まるURLを出力します。

POSTの値はリダイレクトを挟むと消失するので、もし.htaccessでリダイレクトさせたら、
お問い合わせフォームから問い合わせてもSSL対応完了後は一件も問い合わせがこなくなった...
場合によっては500エラー(Non Object Error)になってエラーログが出まくる可能性も...
っていうことがあり得てしまいます。

なので、HTTPからのアクセスはHTTPSにリダイレクトさせつつ、routeやurlヘルパーで書いてきて、
今までhttpとして出力されていた文字列もhttpsとして出力して欲しいですよね？
そんなムシのいい話を、Laravelは実現してくれます。

### 実装

###### Middleware

```bash
php artisan make:middleware ForceHttpProtocol
```

```php:/app/Http/Middleware/ForceHttpProtocol.php
<?php

namespace App\Http\Middleware;
use Closure;

class ForceHttpProtocol {

    public function handle($request, Closure $next) {
        if (!$request->secure() && env('APP_ENV') === 'production') { // 本番環境のみ常時SSL化する
            return redirect()->secure($request->getRequestUri());
        }

        return $next($request);
    }

}
```

###### Kernel

必要箇所以外の記述は省略します

```php:/app/Http/Kernel.php
<?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\ForceHttpProtocol::class, // 追加
    ];
```

### 特定ページのみSSL対応

サーバー証明書のコストの関係とかで、常時SSL化対応ではなく、特定のページだけSSL化することになった場合は次のようにします。

###### Kernel

```php:/app/Http/Kernel.php
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'forceSsl' => App\Http\Middleware\ForceHttpProtocol::class, // 追加
    ];
```

###### Route

```php:/routes/web.php
<?php

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the controller to call when that URI is requested.
|
*/

Route::get('/', 'HomeController@index'); // http

Route::group(['prefix' => 'inquiry', 'as' => 'inquiry', 'middleware' => 'forceSsl'], function(){ // https
    Route::get('/', 'InquiryController@index');
    Route::post('/store', 'InquiryController@store');
    Route::get('/finish', 'InquiryController@finish');
});
```

### 追記:301リダイレクトさせる方法

なんか結構このページが見られているようなので、追記します。
上に書いたhttpsリダイレクトは、302リダイレクトになっています。

なぜなら、secureメソッドが送るデフォルトのステータスコードが302だからです。

```php:/vendor/laravel/framework/src/Illuminate/Routing/Redirector.php

    /**
     * Create a new redirect response to the given HTTPS path.
     *
     * @param  string  $path
     * @param  int     $status
     * @param  array   $headers
     * @return \Illuminate\Http\RedirectResponse
     */
    public function secure($path, $status = 302, $headers = [])
    {
        return $this->to($path, $status, $headers, true);
    }

```

301リダイレクトさせたいならば、下のように、第二引数に301と明示的に書けばそれでOKです。

```php:/app/Http/Middleware/ForceHttpProtocol.php
<?php

namespace App\Http\Middleware;
use Closure;

class ForceHttpProtocol {

    public function handle($request, Closure $next) {
        if (!$request->secure() && env('APP_ENV') === 'production') {
            return redirect()->secure($request->getRequestUri(), 301); // ← ここを変更しました
        }

        return $next($request);
    }

}
```

当たり前すぎるのか、誰も書いてなかったので。