
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、Laravel
だけに限らずですが「フレームワーク」の人気のひとつには、
ログイン機能を簡単にインストールできる
というものがあると思います。
もし「フレームワークをまったく使わずにログイン機能を実装してね」と言われたら、「
Cookie
やSession
、パスワードの暗号化・・・・うーん、やりたくない・・・・」となってしまいます。(ホントにありがたい限りですね
)
ただ、そんな便利なログインですが、「全く別のドメインで運営している2つのサイト間でログインを共通化する」という機能はSession ID
の制約があるためため直接の実装はできません。(チカラ技でできるようですが好ましくないようです)
ただ、このご要望は過去からちらほら聞いていたので、ここいらで何か対策を考えてみることにしました。
そこで
今回はLaravel
で「別ドメインのログインを共通化」する方法をご紹介します。
ぜひ皆さんのお役にたてると嬉しいです
※ なお、ドメインとサブドメイン間なら、Laravel
の設定でログインを共通化することができます。おまけ:ドメイン&サブドメインのログインを共通化するをご覧ください。
「今回はJWTを使ってるので、
セキュリティにも気をつけてます」
開発環境: Laravel 7.x
目次 [非表示]
どのようにして実現するか
冒頭の文章でも書きましたが、通常セッションは別ドメイン間で共有ができないため、直接セッションは触らず以下の手順で自動ログインを実装します。
- サイトAでログイン
- サイトBへ移動するときにトークンをつけて移動
- サイト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用のトレイトを登録する
では、JwtTrait
をUser.php
に登録して使えるようにしておきましょう。
/app/User.php
<?php
namespace App;
use App\Traits\JwtTrait; //
追加
// 省略
class User extends Authenticatable
{
use Notifiable, JwtTrait; //
追加
ミドルウェアの設定をする
では最後にミドルウェアの設定です。
やることは、Laravel
がログインに利用しているミドルウェアAuthenticate
のhandler()
を上書きして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.com
とsub.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 Chrome
のDevTool
で全てのCookie
を削除してから試してみてください。
「Application > Cookies > (あなたのドメイン) > 削除ボタン」
おわりに
ということで、今回はログインを共通化するテクニックをご紹介しました。
この機能を実装すると、いちいち個別にログインをしなくてもよくなるのでとても便利になるんじゃないでしょうか。
また、今回はじめてJWT
を使ってみましたが、URL
のパラメータとして埋め込めるので使い勝手がいいですね。中身のデータが多くなると文字列が長くなってしまいますが、Google Chrome
が許容しているURLの最大の長さはなんと2 MB分なので、不足することはほぼないといっていいでしょう。
もしかすると、JWT
は今後よく使われるテクニックになってくるかもしれませんね。
ぜひ皆さんもチェックしてみてください。
ではでは〜
「チェルシーのアメ、
バターだけの探し中・・・
(コーヒーいらない)」