【Laravel】別ドメイン間でログインを保持する方法

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、Laravelだけに限らずですが「フレームワーク」の人気のひとつには、

ログイン機能を簡単にインストールできる

というものがあると思います。

もし「フレームワークをまったく使わずにログイン機能を実装してね👍」と言われたら、「CookieSession、パスワードの暗号化・・・・うーん、やりたくない・・・・💦」となってしまいます。(ホントにありがたい限りですね😊✨)

ただ、そんな便利なログインですが、「全く別のドメインで運営している2つのサイト間でログインを共通化する」という機能はSession IDの制約があるためため直接の実装はできません。(チカラ技でできるようですが好ましくないようです)

ただ、このご要望は過去からちらほら聞いていたので、ここいらで何か対策を考えてみることにしました。

そこで❗

今回はLaravelで「別ドメインのログインを共通化」する方法をご紹介します。

ぜひ皆さんのお役にたてると嬉しいです😊✨

※ なお、ドメインとサブドメイン間なら、Laravelの設定でログインを共通化することができます。おまけ:ドメイン&サブドメインのログインを共通化するをご覧ください。

「今回はJWTを使ってるので、
セキュリティにも気をつけてます👍」

開発環境: Laravel 7.x

どのようにして実現するか

冒頭の文章でも書きましたが、通常セッションは別ドメイン間で共有ができないため、直接セッションは触らず以下の手順で自動ログインを実装します。

  1. サイトAでログイン
  2. サイトBへ移動するときにトークンをつけて移動
  3. サイトBのミドルウェアでトークンを使って自動ログイン

そして、サイト移動をするときに使うのが今回目玉の「JWT」です。

JWTとは、JSON Web Tokenの略で、簡単にいうと改変できない(改変するとバレる)JSONデータです。詳しくは、Yahooさんが公開しているページをご覧ください。とてもわかりやすくて助かりました😊✨

なお、JWTには有効期限もセットすることができるので、今回はこれを使って「1時間だけ有効なトークン」をつくり、自動ログインを実現してみたいと思います。

では、作業をしていきましょう❗

前提として

最低でもログインに使われるusersテーブルは共通、もしくはコピーされた内容が各DBに保存されていることを前提としています。そして、JWTで送信する内容はユーザーIDです。

なお、別サイトのDBを使ってログインする場合は、次の手順を参考にしてみて下さい。

まず、.envに対象のDB接続情報を登録します。

.env

# 👇 追加
DB_AUTH_HOST=127.0.0.1
DB_AUTH_PORT=3306
DB_AUTH_DATABASE=xxxxxxxx
DB_AUTH_USERNAME=username
DB_AUTH_PASSWORD=password

そして、コンフィグにも新しいDB情報を追加します。

/config/database.php

'mysql' => [ 
    // 省略(ここは元からある部分)
],

// 👇 追加
'auth_mysql' => [
    'driver' => 'mysql',
    'url' => env('DATABASE_URL'),
    'host' => env('DB_AUTH_HOST', '127.0.0.1'),
    'port' => env('DB_AUTH_PORT', '3306'),
    'database' => env('DB_AUTH_DATABASE', 'forge'),
    'username' => env('DB_AUTH_USERNAME', 'forge'),
    'password' => env('DB_AUTH_PASSWORD', ''),

    // 省略

],

最後に、Userモデルにログイン用のコネクションをセットして完了です。

/app/User.php

<?php

// 省略

class User extends Authenticatable
{
    use Notifiable;

    protected $connection = 'auth_mysql'; // 👈 追加

これで、Userモデルにはauth_mysqlが自動的に適用されるようになります。

トレイトをつくる

JWT用のトレイトをつくる

今回は、ログインを共通化するすべてのサイトで

  • JWTトークンをつくる
  • JWTトークンを検証して、データを取り出す

機能が必要になりますので、これらのコードは「トレイト」(いわゆるミックスイン)にまとめておき、使い回しできるようにしておきます。

/app/Traits/JwtTrait.php

<?php

namespace App\Traits;

trait JwtTrait {

    private $secret_key = 'your-secret-key'; // 👈 本来は「.env」に書くべきですが、テストなのでここに書いてます。
    private $jwt_values = [];

    public function getJwtToken() {

        $user_id = $this->id;
        $expiration = time() + 3600; // 1時間
        $header = [
            'typ' => 'JWT',
            'alg' => 'HS256',
            'exp' => $expiration
        ];
        $payload = [
            'user_id' => $user_id
        ];
        $header_token = $this->base64urlEncode(json_encode($header));
        $payload_token = $this->base64urlEncode(json_encode($payload));
        $signature_token = $this->base64urlEncode(
            hash_hmac('sha256', $header_token .'.'. $payload_token, $this->secret_key, true)
        );
        return $header_token .'.'. $payload_token .'.'. $signature_token;

    }

    public function isValidJwtToken($jwt_token) {

        $this->jwt_values = [];
        list($header_token, $payload_token, $signature_token) = explode('.', $jwt_token);
        $header = json_decode($this->base64urlDecode($header_token), true);
        $payload = json_decode($this->base64urlDecode($payload_token), true);
        $signature = $this->base64urlDecode($signature_token);
        $check_signature = hash_hmac('sha256', $header_token .'.'. $payload_token, $this->secret_key, true);

        if(hash_equals($check_signature, $signature)) {

            if($header['exp'] > time()) {

                $this->jwt_values = $payload;
                return true;

            }

            abort(419);

        }

        abort(400, 'Invalid signature');

    }

    public function getJwtValues() {

        return $this->jwt_values;

    }

    private function base64urlEncode($string) {

        $targets = ['+', '/', '='];
        $replacements = ['-', '_', ''];
        return str_replace($targets, $replacements, base64_encode($string));

    }

    private function base64urlDecode($string) {

        $targets = ['-', '_'];
        $replacements = ['+', '/'];
        return base64_decode(str_replace($targets, $replacements, $string) .'=');

    }

}

なお、少し複雑なことをしているように見えるかもしれませんが、JWTの仕様にのっとって以下のデータ加工をしているだけです。

  • getJwtToken(): JWTトークンをつくります
  • isValidJwtToken(): 送信されてきたJWTトークンが正しいか、また期限が切れていないかをチェックします
  • getJwtValues(): チェックが成功した場合にその内容を取得します。
  • base64urlEncode(): Base64エンコードし、さらにURL用に加工します
  • base64urlDecode(): URL用に加工されたBase64エンコードを元に戻します

【注意❗】

JWTは「データ改ざんをチェックできる」テクニックであって、データを暗号化しているわけではないことを覚えておいてください。つまり、誰でもJWTトークンから中身を見ることができます。そのため、パスワードやメールアドレスなどの個人情報はそのまま入れるべきではありません。

JWT用のトレイトを登録する

では、JwtTraitUser.phpに登録して使えるようにしておきましょう。

/app/User.php

<?php

namespace App;

use App\Traits\JwtTrait; // 👈 追加

// 省略

class User extends Authenticatable
{
    use Notifiable, JwtTrait; // 👈 追加

ミドルウェアの設定をする

では最後にミドルウェアの設定です。

やることは、Laravelがログインに利用しているミドルウェアAuthenticatehandler()を上書きしてJWTの自動ログイン部分を追加するだけです。

/app/Http/Middleware/Authenticate.php

<?php

// 省略

class Authenticate extends Middleware
{
    // 省略

    // 👇 追加
    public function handle($request, Closure $next, ...$guards)
    {
        if($request->filled('jwt')) {

            $user = new \App\User();
            $token = $request->jwt;

            if($user->isValidJwtToken($token)) {

                $jwt_values = $user->getJwtValues();
                $user_id = $jwt_values['user_id'] ?? -1;
                auth()->loginUsingId($user_id); // ここで自動ログイン

            }

        }

        return parent::handle($request, $next, $guards); // これは通常のログイン
    }
}

これで全ての設定が完了しました❗

※ なお、これらの設定はログインを共通化する全てのサイトで行ってください。

テストしてみる

では実際にテストをしてみましょう。
まずサイトAで次のようなルートをつくります。

Route::get('link_to_group_website', function(){

    if(auth()->check()) {

        $user = auth()->user();
        $jwt = $user->getJwtToken();
        echo '<a href="http://*****/home?jwt='. $jwt .'">自動ログイン</a>';

    }

});

なお、リンクのURLは「サイトBのログイン必須なページのURL」になります。

では、サイトAにログインします。

そして、ログインした状態で「http://******/link_to_group_website」にアクセスすると、次のようなリンクが表示されます。

では、このリンクをクリックして、サイトBで自動ログインできるかを確認してみましょう!

クリックすると・・・・・・

サイトBでもうまく自動ログインができ、太郎さんの名前が表示されました。
成功です😊✨

おまけ:ドメイン&サブドメインのログインを共通化する

例えば、example.comsub.example.comのログインを共通化する場合です。

セッションを共通で使うフォルダをつくる

セッションデータは他のサイトとの競合を防ぐため、Laravelごとの/storage/framework/sessionsに保存されることになっていますが、これを共通化する必要があります。

そのため、適当な場所にcommon_sessionsなどのフォルダを作成し、さらに書き込み権限を与えておきます。

コンフィグを変更する

次にログインを共通化したい全てのLaravelでコンフィグを変更します。

/config/session.php

'files' => '/(共通セッションフォルダへのパス)/common_sessions',

// 省略

'domain' => '.example.com', // 👈 先頭にドットがついてます!

キーを共通化する

最後に、.envにあるAPP_KEYを全てのサイトで同じものにします。(どのサイトに合わせてもOKです)

APP_KEY=base64://*******************************=

これで完了です。
後はログインが共通化されているかをチェックしてみてください。

上手くいかない場合

もしかすると、すでにCookieが保存されているとうまくいかない場合があります。その場合は、Google ChromeDevToolで全てのCookieを削除してから試してみてください。

「Application > Cookies > (あなたのドメイン) > 削除ボタン」

おわりに

ということで、今回はログインを共通化するテクニックをご紹介しました。

この機能を実装すると、いちいち個別にログインをしなくてもよくなるのでとても便利になるんじゃないでしょうか。

また、今回はじめてJWTを使ってみましたが、URLのパラメータとして埋め込めるので使い勝手がいいですね。中身のデータが多くなると文字列が長くなってしまいますが、Google Chromeが許容しているURLの最大の長さはなんと2 MB分😲❗なので、不足することはほぼないといっていいでしょう。

もしかすると、JWTは今後よく使われるテクニックになってくるかもしれませんね。

ぜひ皆さんもチェックしてみてください。

ではでは〜❗

「チェルシーのアメ、
バターだけの探し中・・・
(コーヒーいらない😂)」

今回の技術をつかった開発のご依頼、お待ちしております😊✨ お問い合わせ また、個人レッスンや、わかりにくい部分がありましたらからお気軽にご連絡ください。 どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly