
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、嬉しいことにこのブログを始めてからご連絡をいただいた訪問ユーザー様と直接お話をさせていただくことも増えてきたのですが、やはりそういった場合には「そうか、じゃあこんなこともできるかも!」という新しいアイデアに気づかせてもらうことあったりします。
そして、今回の話題の出発点になったのが、Laravel + JSでバーコードを読み取る(ダウンロード可)という記事なんですが、これは商品には必ずといっていいほどついている「バーコード」をJavaScriptで読み取るという内容になってます。
ただ、スマートフォンが普及しだしたころからもう1つ存在感を出してきた「読み取りコード」がありますよね。
そうです。
QRコード
ですね。
※ ↓↓↓こういうやつです。
QRコードは縦と横方向に情報をもたせることができるので、情報量がバーコードより多くすることができる、いわば「発展系バーコード」ですが、今回はこのQRコードを読み取ることで自動ログインができたら面白いなと思いサンプルを作ってみることにしました。
(例えば、社員さんがQRコードを配布されていてPCやスマホのカメラに見せるだけでログインできるようなシステムを想定しています)
ぜひ皆さんのお役に立てると嬉しいです
最後に今回のソースコード一式をダウンロードすることができます。
開発環境: Laravel 5.8、Vue 2.6、Google Chrome 75
目次 [非表示]
前提として
この記事の前提として、Laravelのログイン機能がインストール済みであるものとします。もしまだの方は以下の記事を参考にして準備しておいてください。
※ 注意:Google Chromeでテストする場合、ローカルであってもウェブカメラにアクセスするにはHTTPS環境である必要があります。もしまだHTTPSを導入していない方は以下を参考にしてください。
コピペでOK!ローカル環境にHTTPSを導入する(nginx編)
自動ログインの手順
今回、自動ログインを実現する手順は次のとおりになってます。
- JavaScriptでQRコードを読み取る
- 読み取ったデータ(UUID)を持ったユーザーを探す
- 見つかればログインする
こうすることでめんどうなログインフォームへの入力をショートカットできますし、月一回強制的にUUID(他とかぶらないID番号)を変更してQRコードを新しく発行するようにすれば、セキュリティも向上すると思います。
DB内の準備
usersテーブルにUUIDを保存するフィールドを追加する
では、QRコードで読み取る文字列(UUID)を保存しておくフィールドをusers
テーブルに追加しておきましょう。
以下のコマンドでマイグレーションを作成します。
php artisan make:migration add_uuid_to_users_table
すると、database/migrations/****_**_**_******_add_uuid_to_users_table.php
というファイルが作成されますので、このファイルを開いて以下のように変更します。
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddUuidToUsersTable extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('uuid')
->nullable()
->after('remember_token');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('uuid');
});
}
}
変更したらマイグレーションを実行。
php artisan migrate
実行するとテーブルは次のようになります。
テストユーザーを作成する
後で実際にログインのテストをしますのでSeeder
を使ってテストデータを作成します。以下のコマンドを実行してください。
php artisan make:seed UsersTableSeeder
database/seeds/UsersTableSeeder.php
が作成されるので、run()
内に以下のコードを追加します。
public function run()
{
$names = [
'taro' => '太郎',
'jiro' => '次郎',
'saburo' => '三郎',
'shiro' => '四郎',
'goro' => '五郎',
'rokuro' => '六郎',
'shichiro' => '七郎',
'hachiro' => '八郎',
'kuro' => '九郎'
];
foreach ($names as $name_en => $name_jp) {
\App\User::create([
'name' => $name_jp,
'email' => $name_en .'@example.com',
'password' => bcrypt('xxxxxxxx'),
'uuid' => (string) \Str::uuid() // ここがUUID
]);
}
}
変更したらUsersTableSeeder
が有効になるようにdatabase/seeds/DatabaseSeeder.php
のrun()
内に登録します。(コメントアウトを外すだけでOKです)
public function run()
{
$this->call(UsersTableSeeder::class);
}
ではSeeder
でテストユーザーを登録しましょう。(今回はテーブルを削除して一気に実行します)
php artisan migrate:fresh --seed
実行が完了すると、以下のようにusers
テーブルの各ユーザーにUUID
が登録されます。
自動ログイン機能をつくる
ではここからが実際にコードを書いていく作業になります。
QRコードを読み取る部分をつくる
ログインフォームの代わりとして、QRコードを読み取るページをつくっていきましょう。
ルートをつくる
はじめにroute/web.php
にルートを追加します。
Route::get('auth/qr_login', 'Auth\\QrLoginController@showQrReader'); // ログインフォーム
Route::post('auth/qr_login', 'Auth\\QrLoginController@login'); // Ajax通信
コントローラーをつくる
続いて、ルートで指定したコントローラーを以下のコマンドで作成します。
php artisan make:controller Auth\\QrLoginController
app/Http/Controllers/Auth/QrLoginController.php
が作成されるので以下のようにメソッドを追加します。
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class QrLoginController extends Controller
{
public function showQrReader() {
return view('auth.qr_login'); // ログインフォームの代わり
}
}
ビューをつくる
では、ビューがまだないのでresources/views/auth/qr_login.blade.php
にファイルを作成し、中身を以下のようにして保存してください。
<html>
<head>
<style>
canvas {
padding-left: 0;
padding-right: 0;
margin-left: auto;
margin-right: auto;
display: block;
width: 50%;
}
</style>
</head>
<body>
<div id="app">
<div style="text-align:center;font-size:35px;">QRコードを読みとって自動ログインできます</div>
<br>
<canvas ref="canvas"></canvas>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
<script>
new Vue({
el: '#app',
data: {
video: null,
canvas: null,
context: null,
uuid: '',
completed: false,
componentWidth: -1,
},
computed: {
hasUuid() {
return (this.uuid !== '');
}
},
methods: {
renderFrame() {
if(!this.hasUuid && !this.completed) { // まだQRコードが読み込まれていない場合
const video = this.video;
const canvas = this.canvas;
const context = this.context;
if(video.readyState === video.HAVE_ENOUGH_DATA) {
context.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height);
if(code) {
this.uuid = code.data;
axios.post('/auth/qr_login', { uuid: this.uuid })
.then((response) => {
const result = response.data.result;
const user = response.data.user;
if(result) {
this.completed = true;
alert('「'+ user.name +'」さん、おはようございます!');
// location.href = '/user'; // ここでリダイレクト
} else {
console.log('ログイン失敗..');
}
})
.catch(error => {
console.log(error);
})
.finally(() => {
this.uuid = '';
});
}
}
}
requestAnimationFrame(this.renderFrame);
},
initializeVideo(videoParams) {
return navigator.mediaDevices.getUserMedia(videoParams)
},
initializeVideoThen(stream) {
this.video.srcObject = stream;
this.video.play();
}
},
mounted() {
this.video = document.createElement('video');
this.video.addEventListener('loadedmetadata', () => {
this.canvas.width = this.componentWidth;
this.canvas.height = Number(this.canvas.width * this.video.videoHeight / this.video.videoWidth);
this.renderFrame();
});
this.video.setAttribute('autoplay', '');
this.video.setAttribute('muted', '');
this.video.setAttribute('playsinline', '');
this.canvas = this.$refs.canvas;
this.context = this.canvas.getContext('2d');
this.componentWidth = this.canvas.offsetWidth;
const videoParams = {
audio: false,
video: {
facingMode: {
exact: 'environment'
},
width: { ideal: 1080 },
height: { ideal: 720 }
}
};
this.initializeVideo(videoParams)
.then(this.initializeVideoThen)
.catch(() => {
this.initializeVideo({ video: true })
.then(this.initializeVideoThen)
});
}
});
</script>
</body>
</html>
【追記:2019.07.30】バグがあることがわかりましたので変更しました。すみません。
【追記:2022.7.4】負荷を軽減するようコードを修正しました。
【追記:2022.7.12】iPhone でうまく動かないためコードを大幅に修正しました。(iOS 15.5 の Safari で動作を確認)
なお、QRコードの読み取りには jsQR というJSパッケージを使っているのですが、このパッケージはcdn
が存在していないので、今回はpublic/js
にgit clone
で設置して使います。(本来はnpm
でインストールしてビルドすべきですが、説明が複雑になってしまうので今回は割愛します)
【追記:2022.7.12】cdn が公開されてましたので変更しました。
コマンドラインでpublic/js
に移動して以下のコマンドを実行してください。(もしくはシンプルにダウンロードして設置するだけでもOKです)
git clone https://github.com/cozmo/jsQR.git
設置が完了するとこのようになります。
なお、このビューの中でやっていることは以下のとおりです。
- mounted()内でvideoやcanvasを準備し、ウェブカメラにアクセスする
- ウェブカメラにアクセスができたらrenderFrame()で繰り返し、内容を描画する
- もし描画中にQRコードが読み込まれたらaxiosを使ってAjax送信
- 送信されてきたUUIDでログインができたらアラートを表示
- (ログインに失敗したら)2〜4を繰り返す
そして、実行したものがこちらです(ウェブカメラにはシールを貼ってある状態です)
ログイン部分をつくる
残るは、Ajax通信でUUID
が送信されてきたときにログインを実行する部分です。
先ほど作成したQrLoginController
を開いてlogin()
を追加してください。
public function login(Request $request) {
$result = false;
$user = \App\User::where('uuid', $request->uuid)->first();
if(!is_null($user)) {
\Auth::login($user); // ユーザーをログインさせる
$result = true;
}
return [
'result' => $result,
'user' => $user
];
}
中身としてはシンプルで、送信されてきたUUID
を使ってユーザーをDBから探し出し、もしユーザーが存在していたらAuth::login()
で強制的にログインさせるという流れになっています。
テストしてみる
では、お待たせしました。
ここまでで開発してきたコードで自動ログインを実行してみたいと思います。
なお、読み取るQRコードはテストユーザーの「太郎さん」のUUID
で、QRコードの画像は、いつも使わせてもらっているQRのススメさんでスマホをつかって作成することにします。
・・・・・・ということで、実行結果はこうなりました!
↓↓
↓↓
↓↓
はい!
うまくログインすることができました
実際はログインできたらユーザー専用ページへリダイレクトする形になると思いますが、今回はテストなのでここで終了にしたいと思います。
お疲れ様でした!
ソースコードをダウンロードする
今回実際に開発したソースコード一式を以下からダウンロードすることができます。
※ ただし、jsQRのインストールやDBテーブルの準備はご自身で実行していただく必要があります。
【Laravel + JavaScript】QRコードで自動ログインする機能
【追記:2019.07.30】バグがあることがわかりましたので変更しました。すみません。
おわりに
今回、QRコードの読み取りをJavaScriptのみで実行してみましたが、感想としては「えっ、こんなに反応早いの!?」でした。
正直なところ、ブラウザだとどうしても動作がモサッとしてしまって、ユーザビリティとしてはあんまり・・・なんてことになるのかと思いきや、あまりにQRコードの読み取りが早くてテスト画像は何回も取り直ししました(というのもQRコードは一部分がかけていてもデータを読み取れるので、QRコードが画面の真ん中に来ないのに自動ログインが完了してしまったんです)
ということで今回はQRコードを読みとって自動ログインするサンプルを作ってみました。
このテクニックを使えば、出勤チェックシステムも使えるでしょうし、Raspberry Pi
と連携させて入室管理システムなんていうのもつくれるんじゃないでしょうか。
テクノロジーの進歩ってすごいですね。
ではでは〜!