JWTを利用した認証をLaravelで実装することができるライブラリのjwt-authを使用して、Vue+Laravelアプリの認証機能を作成したので、メモがてら手順をまとめてみました。
VueRouterのbeforeEach
を使用した閲覧制限機能も実装します。
SPAで認証機能を実装される方の参考になれば嬉しいです😄
こちらの記事で紹介しているような手順で、VueとLaravelの連携が完了している前提で進めていきます!
バージョン
composer: 2.2.7
php: 7.4.28
laravel: 6.20.44
npm: 6.14.12
node: 14.16.1
vue: 2.5.17
vue-router: 3.1.3
認証機能の流れ
- 今回の認証機能は、以下のような流れとなっています。
-
ログインAPI成功時に、バックエンド(Laravel)からJWTが発行されるので、フロントエンド(Vue)でローカルストレージにJWTを保存します
-
axiosを利用したAPIリクエスト時に、
Authorization
,Accept
,Content-Type
の3つのヘッダーを付与する。そのうちの1つAuthorization
に、ローカルストレージに保存したJWTを持たせます -
ミドルウェア(
auth:api
)を適用したルートグループに入っているAPIを実行した際は、Laravel側でjwt-authによるJWTのチェック処理が走ります。JWTが不正な場合は401エラーを出し、正常の場合は正常なレスポンスを返します -
axiosのレスポンス時のインターセプターで、もし401エラーが返ってきた場合は
1
でローカルストレージに保存したJWTを破棄して、ログイン画面へ遷移させます -
ログアウトAPI実行時には、Laravelが保持するJWTを破棄するので、フロント側でもローカルストレージに保存したJWTを破棄します
JWT Authをインストール
- JWT(JSON Web Token)を使用して認証を行うことができるライブラリJWT Authをインストールします
composer require tymon/jwt-auth:1.0.0-rc.5
- 以下コマンドも実行してください
composer require lcobucci/jwt:3.3.3
※ 👆を実行していない場合、jwt-authで使用しているライブラリlcobucci
のバージョン依存により以下のようなエラーが起きます
Could not create token: Using integers for registered date claims is deprecated, please use DateTimeImmutable objects instead.
- 以下コマンドで、JWT Auth設定ファイル
config/jwt.php
を作成します
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
シークレットキーを生成
- 以下コマンドを実行して、JWTを復元/生成する為に必要なシークレットキーが発行されます
php artisan jwt:secret
-
.env
ファイルにJWT_SECRET
という値が追加されていれば正常にシークレットキーが発行できています
Userモデルの編集
- Userモデル(
User.php
)でJWT Authを使う為に必要な記述を追加していきます -
use
にTymon\JWTAuth\Contracts\JWTSubject
を追加します
use Tymon\JWTAuth\Contracts\JWTSubject; // 追加
- クラス宣言部に
implements JWTSubject
を追加します
// 変更前
class User extends Authenticatable
// 変更後
class User extends Authenticatable implements JWTSubject
- 以下のメソッドを追加します
public function getJWTIdentifier()
{
return $this->getKey();
}
// JWTのペイロードに何か値を追加したい場合はここで追加
public function getJWTCustomClaims()
{
return [];
}
config/auth.php
の編集
- 認証の設定が記載されているファイル
config/auth.php
を編集していきます
defaults
を編集
// 変更前
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
// 変更後
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
guards
を編集
// 変更前
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
// 変更後
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
AuthController
の作成
- 認証関係のコントローラーとして
AuthController
を作成していきます - 以下コマンドで
AuthController
を作成します
php artisan make:controller AuthController
- 作成された
AuthController
を以下のように編集します - こちらはjwt-auth公式ドキュメントを参考に作成しました
<?php
namespace App\Http\Controllers;
class AuthController extends Controller
{
public function login()
{
$credentials = request(['email', 'password']);
if ($token = auth('api')->attempt($credentials)) {
return $this->respondWithToken($token);
}
return response()->json(['error' => 'Unauthorized'], 401);
}
public function logout()
{
auth()->logout();
return response()->json(['message' => 'Successfully logged out']);
}
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth("api")->factory()->getTTL() * 60
]);
}
}
api.php
の編集
- 認証が必要なAPIは、ミドルウェア(
auth:api
)を適用したルートグループに入れるておきます(後ほどAPI認証のテストとして使用するAPIのルートも追加しています) - このグループに入れたAPIが実行された場合は、リクエスト時に認証チェックが走るようになります
// 認証が不要なAPIはグループ外に記述
Route::post('/login', 'AuthController@login');
Route::get('/for_everyone', 'TestController@forEveryone');
// 認証が必要なAPIは以下グループ内に記述
Route::group(['middleware' => 'auth:api'], function () {
Route::get('/requires_auth', 'TestController@requiresAuth');
Route::post('/logout', 'AuthController@logout');
});
API認証のテストとして使用するAPIを作成
-
TestController
を作成します
php artisan make:controller TestController
- 作成した
TestController
で、文字列を返すメソッドを2つ用意しておきます
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TestController extends Controller
{
public function forEveryone () {
return '認証が不要なAPIです';
}
public function requiresAuth () {
return '認証が必要なAPIです';
}
}
これでLaravel側の設定は終了です。以下でVue側の設定をしていきます!
VueRouterのbeforeEachで閲覧制御を実装
- VueRouterでは、ルート定義時に
meta
を追加することでメタフィールドを定義することができます。認証が必要な画面のルートには、meta
を追加して、requiresAuth: true
を追加しましょう
import VueRouter from "vue-router";
import Home from "./components/Home";
import Login from "./components/Login";
import RequiresAuth from "./components/RequiresAuth";
const routes = [
{
path: "/login",
component: Login,
name: "LOGIN",
},
{
path: "/home",
component: Home,
name: "HOME",
},
{
path: "/requires_auth",
component: RequiresAuth,
name: "REQUIRES_AUTH",
// 認証が必要なルートには以下を追加
meta: {
requiresAuth: true,
},
},
{
path: "*",
redirect: {
name: "HOME"
}
},
];
const router = new VueRouter({
mode: "history",
routes,
});
export default router;
beforeEachを実装
beforeEachとは
- ルート遷移時に挟む処理を書くことができるVueRouterの機能です。
- 引数は
to
(どのルートに遷移しようとしているか)と、from
(どのルートから遷移しようとしているか)と、next
(toへ実際に遷移するという関数) の3つです。next()
を実行することで、目的のルートへの遷移処理を実行することができます。
beforeEach内の処理の流れ
- 目的のルートのメタフィールドに
requiresAuth: true
があるかどうかを調べる - もし遷移先のmetaの中に
requiresAuth: true
がなければ、next()
を実行して目的のルートに遷移 - もし遷移先のmetaの中に
requiresAuth: true
があれば、ローカルストレージからjwtトークンを取得して、有効期限を見る - もし、JWTが存在しない、または有効期限が切れている場合は、ローカルストレージのトークンを削除してログイン画面へ遷移
- もし有効期限が切れていない場合は、
next()
を実行して目的のルートに遷移
-
router.js
ファイルのconst router = new VueRoute
とexport default router;
の間に以下のbeforeEachを追加してください
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
// ルートのmeta.requiresAuthがtrueの場合、ローカルストレージからJWTを取得
const token = localStorage.getItem("authorization_token");
if (!token) {
// jwtトークンがない場合はキャッシュを破棄してログインページに遷移
console.error('認証エラー!!');
localStorage.removeItem("authorization_token");
next({ name: "LOGIN" });
return;
}
const base64Url = token.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const decodedToken = JSON.parse(
decodeURIComponent(escape(window.atob(base64)))
);
const expireDate = new Date(decodedToken.exp * 1000);
const now = new Date();
const isValidToken = now < expireDate;
if (isValidToken) {
// jwtが有効期限内の場合は通常の遷移
next();
} else {
// jwtの有効期限が切れている場合は、キャッシュを破棄してログインページに遷移
console.error('認証エラー!!');
localStorage.removeItem("authorization_token");
next({ name: "LOGIN" });
}
} else {
// 目的のルートが認証を必要としない場合はJWTのチェックを行わない
next();
}
});
axiosインターセプターの実装
- JWT Authを入れたことにより、認証が必要なAPIを叩く際は、以下3つのヘッダーを追加しなければエラーになるようになりました
Authorization: 'Bearer {JWT}' // 認証時に発行されたJWT
Accept: 'application/json' // 固定値
Content-Type: 'application/json' // 固定値
- axiosでAPIリクエストを送る処理を書く度に上記ヘッダーを定義するのは面倒なので、今回はaxiosのインターセプターを利用します。
import axios from 'axios';
axios.interceptors.request.use((request) => {
// リクエスト送信前の処理
return request
});
axios.interceptors.response.use((response) => {
// レスポンス受信時の処理(成功時)
return response;
}, (error) => {
// レスポンス受信時の処理(エラー発生時)
return error;
});
- 今回、インターセプターで共通化したい処理は以下の二つです。
- リクエストを送る際に3つのヘッダーを追加する
- レスポンスが401エラーだった場合にキャッシュを破棄してログイン画面へ遷移させる
- 1の処理はリクエスト送信時のインターセプターに記載して、2はレスポンス受信時のインターセプターに記載していきます。
import axios from 'axios';
// axiosリクエストインターセプター
axios.interceptors.request.use((request) => {
// ローカルストレージからauthorization_tokenを取得
const token = localStorage.getItem('authorization_token');
// ヘッダーに必要な値を追加
request.headers = {
'Authorization': token ? `Bearer ${token}` : '',
'Accept': "application/json",
'Content-Type': 'application/json'
};
return request
});
// axiosレスポンスインターセプター
axios.interceptors.response.use((response) => {
// 成功時の処理
return response;
}, (error) => {
// エラー発生時の処理
// 401の場合キャッシュを削除してloginページに飛ばす
if (error.response.status === 401) {
console.error('認証エラー!!');
localStorage.removeItem('authorization_token');
router.push({
name: 'LOGIN'
});
}
return error;
});
今回は以下3つのコンポーネントを用意して、それぞれ機能を追加していきます。
ログイン画面(Login.vue
)の実装
- ログインボタン
<template>
<div>
<div class="container">
<h2>ログインフォーーーム</h2>
<input class="form-control" type="text" v-model="email" placeholder="Enter email"/>
<input class="form-control" type="password" v-model="password" placeholder="Password"/>
<button class="btn btn-primary" type="button" @click="login()">LOGIN</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
email: "",
password: "",
};
},
methods: {
login() {
axios.post("/api/login", {
email: this.email,
password: this.password,
}).then((res) => {
const token = res.data.access_token;
// JWTをローカルストレージに保存
localStorage.setItem("authorization_token", token);
// メニュー一覧へ遷移
this.$router.push({
name: "HOME"
});
}).catch((err) => {
console.error(err);
});
},
},
};
</script>
<style scoped>
.container {
margin: auto;
width: 60%;
padding: auto;
}
</style>
- Vue+Laravelで認証機能を実装する記事の多くがJWTをキャッシュする際にVuexを利用している記事が多い印象でした。
- Vuexはリロードでデータが消えるので、JWTが消えてしまう為、今回はローカルストレージを使用してみました。
ローカルストレージとは
- ブラウザ内のデータを保存する領域。
- クッキーとの違いは、保存期間の有無。クッキーは保存する期間を指定する必要があるが、ローカルストレージはデータを削除する処理を実行しない限り、半永久的にデータを保持する。(保存できるデータ容量も差がある。Cookieは4KBでローカルストレージは5MB)
- 検証ツールを開き、「アプリケーション」を選択し、「ストレージ」セクションにある「ローカルストレージ」を選択すれば保存されたデータを見れます。
認証が必要な画面(RequiresAuth.vue
)の実装
- クリックイベントでログアウトAPIを実行しましょう
- 処理成功時に、ログイン時にローカルストレージに保存したJWTを破棄して、ログイン画面へ遷移させる処理を書く
<template>
<div class="container">
<h2>認証が必要な画面</h2>
<button @click="logout()" style="color:red;">ログアウト</button>
</div>
</template>
<script>
export default {
data () {
return {
apiData: ''
};
},
methods: {
// ログアウト処理
logout () {
axios.get('/api/logout').then((response) => {
// ローカルストレージからJWTを破棄
localStorage.removeItem('authorization_token');
// ログイン画面へ遷移
this.$router.push({
name: 'LOGIN'
});
}).catch((error) => {
console.error(error);
});
}
}
}
</script>
<style scoped>
.container {
margin: auto;
width: 60%;
padding: auto;
}
</style>
認証が不要な画面(Home.vue
)の実装
<template>
<div class="container">
<h2>認証が不要な画面</h2>
<router-link :to="{name:'REQUIRES_AUTH'}">認証が必要な画面に遷移</router-link><br>
<button @click="executeApiRequiresAuth()">認証が必要なAPIを実行</button>
<button @click="executeApi()">認証が不要なAPIを実行</button>
<h3>Data: <span style="color:red;">{{ apiData }}</span></h3>
</div>
</template>
<script>
export default {
data () {
return {
apiData: ''
};
},
methods: {
executeApi () {
axios.get('/api/for_everyone').then((response) => {
this.apiData = response.data;
}).catch((error) => {
console.error(error);
});
},
executeApiRequiresAuth () {
axios.get('/api/requires_auth').then((response) => {
this.apiData = response.data;
}).catch((error) => {
console.error(error);
});
}
}
}
</script>
<style scoped>
.container {
margin: auto;
width: 60%;
padding: auto;
}
</style>
システムが整ったので、ユーザーデータを追加してみましょう
- データベースとの連携はできている前提で話をしていきます🙇
- usersテーブルのマイグレーションはデフォルトで作成されているので、以下コマンドでusersテーブルを作成します
php artisan migrate
- 以下コマンドでユーザーテーブルのシーダーを作成します
php artisan make:seeder UsersTableSeeder
- 作成された
UsersTableSeeder
を編集していきます
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Seeder;
class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
DB::table('users')->insert([
'name' => 'TestUser',
'email' => 'test@test.com',
'password' => bcrypt('password'),
]);
}
}
- 以下コマンドでシーダー(
UsersTableSeeder
)を実行します
php artisan db:seed --class=UsersTableSeeder
実際に動かしてみましょう
ログインしていない状態
- まずは
/home
にアクセスしてみます
- 「認証が不要なAPIを実行」ボタンを押下して、認証が不要なAPIを実行してみましょう
- 問題なく実行できます
-
「認証が必要なAPIを実行」ボタンを押下して、認証が必要なAPIを実行しましょう
-
laravelから401エラーが返り、ログイン画面に遷移するはずです
-
「認証が必要な画面に遷移」リンクを押下して、認証が必要な画面(
/requires_auth
)に遷移してみましょう -
beforeEachのガードに引っかかり、ログイン画面に遷移するはずです
ログインしている状態
- ログイン画面
/login
にアクセスしてみます - 作成したユーザーデータの情報をフォームに入力して、「ログイン」ボタンを押下します
- ログインが成功し、
/home
に遷移するはずです
- ログインAPIを実行した際に発行されたJWTが、ローカルストレージに登録されていることが確認できます。
- 「認証が必要なAPIを実行」ボタンを押下して、ログインしていない状態では実行できなかった認証が必要なAPIを実行してみましょう
- ログインしているので、問題なくAPIが実行できます
- 「認証が必要な画面に遷移」リンクを押下して、認証が必要な画面(
/requires_auth
)に遷移してみましょう - ログインしているので、beforeEachのガードに引っかからずに遷移します
- ログアウトボタンを押してみましょう
- ローカルストレージからjwtトークンが削除され、ログイン画面に遷移します
実装完了🔥
少し長くなりましたが、以上でLaravelとVueを連携したアプリの認証機能の実装が完了しました!
ご指摘、質問等ありましたら、是非コメントお願いします。
読んでいただき、ありがとうございました!