はじめに
普通、全ページをSSL化(https)にするなら.htaccessにこんな風に書きますよね。
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
静的に作ってるページしかないならこれだけで全部httpsにリダイレクトするので、なんだかよさそうな気がします。
しかし、この書き方はformが絡むと結構深刻な問題を引き起こします。
Laravelで簡単なお問い合わせフォームを作ったとして、次のような書き方をしていた場合
{{ Form::open(['url' => route('inquiry.store'), 'method' => 'POST']) }}
{{ csrf_token() }}
{{ Form::text('name', NULL) }}
{{ Form::submit('submit') }}
{{ Form::close() }}
実際には次のような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
php artisan make:middleware ForceHttpProtocol
<?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
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
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
/*
|--------------------------------------------------------------------------
| 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だからです。
/**
* 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
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);
}
}
当たり前すぎるのか、誰も書いてなかったので。