長いこと開発者をやっていると自分が開発した古いphpのプログラムの管理が悩みの種になります。なぜならLaravelのフレームワークが使われていないからです。年々新しい機能を追加したりしていると複雑になってくるし、ある昔のフレームワークで書かれたプロジェクトではphpunitも使えなく検証も容易にできません。このままで進んでいくにはますます管理において頭が痛くなります。しかし、すべて書き直してLaravelへの移行となると、日数とコストは大変なものです。さらに、外見がほとんど変わらずに中身だけの変更なので、お客さんを説得するのも難しい。

しかし、機会は訪れるもので、その悩みのプロジェクトのひとつにおいて、お客さんから大きな新規のリクエストが来ました。その新規のリクエストは現在の機能とは独立して扱える部分が多いので、こちらを独立させてLaravelで開発することにして、完成したら現行の機能を徐々にLaravelに移行していく、ということになりました。

嬉しい前進ですが、このプロジェクトに関して1つの必須条件は、現行のプログラムの認証を使用して(まだ使われるので)、新規のLaravelのプログラムの方では再度ログインを必要としてはいけない、というものです。

現行のプログラムの認証セッション

現行のプログラムもLaravelと同様に、ユーザー認証後ブラウザのクッキーを使用してそのユーザーのためだけの認証セッションを作成します。

以下は、現行のプログラムの認証セッションをシミュレートするプログラムですが、ログインしたユーザーの情報(ここでは、user_id)をセッションの情報に保存します。

ini_set('session.save_path', "/usr/www/repos/session");
ini_set('session.cookie_secure', '1'); // only secure
ini_set('session.name', 'admin');

session_start();
$_SESSION = ['user_id' => 1];

このプログラムをウェブでアクセスすると、アクセスしたクライアント、つまりブラウザでは、そのドメインで、adminという名前のクッキーが作成され、t20fa2tc05dd5gda1sfs1tsrofのような値が保存されます。そして、サーバー側では、指定した場所、つまり、/usr/www/repos/sessionのディレクトリに、sess_t20fa2tc05dd5gda1sfs1tsrofのセッションファイルが作成されます。その中身は以下のようになります。phpの$_SESSION変数の中身がそこで保存されています。

$ sudo cat /usr/www/repos/session/sess_t20fa2tc05dd5gda1sfs1tsrof
user_id|i:1;

認証セッションの認識

さて、問題は先の他のphpプログラムでの認証セッションをどうやってLaravelで認識して、あたかも自分の認証とするか、です。しかし、phpのセッションの構造は、Laravelの認証後に作成されるセッションとは違い、Laravelで簡単に共有というわけにはいきません。

そこで、私が考えたのは、まず、新規のLaravelのプログラムが認証を必要するときに、現行のプログラムですでに作成された認証セッションを認識して、それが正しいものなら、改めてLaravelの認証を実行することです。幸い、Auth::login($user);のようにログイン画面を通さずに認証する関数もあります。

まず、routeの設定部分を見てみましょう。

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

上では、/homeにアクセスすると、ログイン画面、/loginにリダイレクトされますが、それはHomeControllerのコンストラクタで以下のようにガードされているからです。


namespace App\Http\Controllers;

use Illuminate\Http\Request;

class HomeController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:admin'); // ここで保護
    }

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        return view('home');
    }
}

しかし、以下のようにAuthenticateのミドルウェアのauthenticate()を上書き定義すると、


namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;

use Auth;
use App\User;

class Authenticate extends Middleware
{
    /**
     * Determine if the user is logged in to any of the given guards.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  array  $guards
     * @return void
     *
     * @throws \Illuminate\Auth\AuthenticationException
     */
    protected function authenticate($request, array $guards)
    {
        if (empty($guards)) {
            $guards = [null];
        }

       if (in_array('admin', $guards)) {
            // すでにLaravel側で認証されているなら、OK
            if ($this->auth->guard('admin')->check()) {
                return $this->auth->shouldUse('admin');
            }

            // 現行のプログラムの認証セッションから情報を取得

            ini_set('session.save_path', "/usr/www/repos/session");
            ini_set('session.cookie_secure', '1'); // only secure
            ini_set('session.name', 'admin');

            session_start();

            if (isset($_SESSION['user_id'])) {
                if ($user = User::find($_SESSION['user_id'])) {
                    // ユーザーが存在するなら、Laravelで認証
                    Auth::guard('admin')->login($user);
                }
            }
        }

        foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {
                return $this->auth->shouldUse($guard);
            }
        }

        $this->unauthenticated($request, $guards);
    }
...

すでに、現行のプログラムで認証されているなら、そのセッションの値を取得してLaravel側でも認証とします。

注意点としては、現行のプログラムと新規のLaravelのプログラムとで2つの認証セッションが存在することになります。両方とも違うセッションでお互いの存在を知らないので、セッションの期間が異なることになります。現行の方の認証が期限切れとなってもLaravelの方ではまだ期限内かもしれません。しかし、逆にLaravelのセッションが期限切れとなっても現行のプログラムのセッションが期限内なら、再度Laravelでセッションを作成するので問題はありません。

それから、現行のプログラムもLaravel側でも認証されていなときに、Laravel側にアクセスされたら、現行のプログラムのログイン画面にリダイレクトすることが必要です。それは以下のように、先のAuthenticateのクラスで、unauthenticated()を定義することにより可能です。

...
   /**
     * Handle an unauthenticated user.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  array  $guards
     * @return void
     *
     * @throws \Illuminate\Auth\AuthenticationException
     */
    protected function unauthenticated($request, array $guards)
    {
        if (in_array('admin', $guards)) {
           throw new AuthenticationException(
                'Unauthenticated.', $guards, 'http://localhost/app/admin/login' //現行のログイン画面
            );
        }
    }
...

最後に、現行のプログラムも新規のLaravelでも、セッションのクッキーパスが同じとすることをお忘れずに。そうでないと、新規のLaravelでは現行のプログラムのセッションにアクセスできません。上では両方ともクッキーパスはデフォルトの/となっています。

By khino