6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Laravel6 / Vue2】Laravel6+Vue.jsでJWT認証機能を実装

Last updated at Posted at 2022-06-19

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

認証機能の流れ

  • 今回の認証機能は、以下のような流れとなっています。

スクリーンショット 2022-06-13 1.36.21.png

  1. ログインAPI成功時に、バックエンド(Laravel)からJWTが発行されるので、フロントエンド(Vue)でローカルストレージにJWTを保存します

  2. axiosを利用したAPIリクエスト時に、Authorization,Accept,Content-Typeの3つのヘッダーを付与する。そのうちの1つAuthorizationに、ローカルストレージに保存したJWTを持たせます

  3. ミドルウェア(auth:api)を適用したルートグループに入っているAPIを実行した際は、Laravel側でjwt-authによるJWTのチェック処理が走ります。JWTが不正な場合は401エラーを出し、正常の場合は正常なレスポンスを返します

  4. axiosのレスポンス時のインターセプターで、もし401エラーが返ってきた場合は1でローカルストレージに保存したJWTを破棄して、ログイン画面へ遷移させます

  5. ログアウト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を使う為に必要な記述を追加していきます
  • useTymon\JWTAuth\Contracts\JWTSubjectを追加します
User.php
use Tymon\JWTAuth\Contracts\JWTSubject; // 追加
  • クラス宣言部にimplements JWTSubjectを追加します
User.php
// 変更前
class User extends Authenticatable

// 変更後
class User extends Authenticatable implements JWTSubject

  • 以下のメソッドを追加します
User.php
public function getJWTIdentifier()
{
    return $this->getKey();
}

// JWTのペイロードに何か値を追加したい場合はここで追加
public function getJWTCustomClaims()
{
    return [];
}

config/auth.phpの編集

  • 認証の設定が記載されているファイルconfig/auth.phpを編集していきます

defaults を編集

auth.php
// 変更前
'defaults' => [
    'guard' => 'web',
    'passwords' => 'users',
],
// 変更後
'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

guardsを編集

auth.php
// 変更前
'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.php
<?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.php
// 認証が不要な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つ用意しておきます
TestController.php
<?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で閲覧制御を実装

スクリーンショット 2022-06-13 1.36.57.png

  • VueRouterでは、ルート定義時にmetaを追加することでメタフィールドを定義することができます。認証が必要な画面のルートには、metaを追加して、requiresAuth: true を追加しましょう
router.js
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内の処理の流れ

  1. 目的のルートのメタフィールドにrequiresAuth: trueがあるかどうかを調べる
  2. もし遷移先のmetaの中にrequiresAuth: true がなければ、next() を実行して目的のルートに遷移
  3. もし遷移先のmetaの中にrequiresAuth: true があれば、ローカルストレージからjwtトークンを取得して、有効期限を見る
  4. もし、JWTが存在しない、または有効期限が切れている場合は、ローカルストレージのトークンを削除してログイン画面へ遷移
  5. もし有効期限が切れていない場合は、next()を実行して目的のルートに遷移
  • router.jsファイルのconst router = new VueRouteexport default router;の間に以下のbeforeEachを追加してください
router.js
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;
});
  • 今回、インターセプターで共通化したい処理は以下の二つです。
  1. リクエストを送る際に3つのヘッダーを追加する
  2. レスポンスが401エラーだった場合にキャッシュを破棄してログイン画面へ遷移させる
  • 1の処理はリクエスト送信時のインターセプターに記載して、2はレスポンス受信時のインターセプターに記載していきます。
app.js
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つのコンポーネントを用意して、それぞれ機能を追加していきます。

スクリーンショット 2022-06-19 2.51.59.png

ログイン画面(Login.vue)の実装

  • ログインボタン
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)の実装

  1. クリックイベントでログアウトAPIを実行しましょう
  2. 処理成功時に、ログイン時にローカルストレージに保存したJWTを破棄して、ログイン画面へ遷移させる処理を書く
RequiresAuth.vue
<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)の実装

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を編集していきます
UsersTableSeeder.php
<?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にアクセスしてみます

スクリーンショット 2022-06-19 4.06.22.png

  • 「認証が不要なAPIを実行」ボタンを押下して、認証が不要なAPIを実行してみましょう
  • 問題なく実行できます

スクリーンショット 2022-06-19 4.07.01.png

  • 「認証が必要なAPIを実行」ボタンを押下して、認証が必要なAPIを実行しましょう

  • laravelから401エラーが返り、ログイン画面に遷移するはずです

  • 「認証が必要な画面に遷移」リンクを押下して、認証が必要な画面(/requires_auth)に遷移してみましょう

  • beforeEachのガードに引っかかり、ログイン画面に遷移するはずです

ログインしている状態

  • ログイン画面/loginにアクセスしてみます
  • 作成したユーザーデータの情報をフォームに入力して、「ログイン」ボタンを押下します
  • ログインが成功し、/homeに遷移するはずです

スクリーンショット 2022-06-19 4.08.08.png

スクリーンショット 2022-06-19 4.06.22.png

  • ログインAPIを実行した際に発行されたJWTが、ローカルストレージに登録されていることが確認できます。

スクリーンショット 2022-06-19 4.10.02.png

  • 「認証が必要なAPIを実行」ボタンを押下して、ログインしていない状態では実行できなかった認証が必要なAPIを実行してみましょう
  • ログインしているので、問題なくAPIが実行できます

スクリーンショット 2022-06-19 4.08.47.png

  • 「認証が必要な画面に遷移」リンクを押下して、認証が必要な画面(/requires_auth)に遷移してみましょう
  • ログインしているので、beforeEachのガードに引っかからずに遷移します

スクリーンショット 2022-06-19 4.09.36.png

  • ログアウトボタンを押してみましょう
  • ローカルストレージからjwtトークンが削除され、ログイン画面に遷移します

スクリーンショット 2022-06-19 2.39.54.png

スクリーンショット 2022-06-19 4.11.07.png

実装完了🔥

少し長くなりましたが、以上でLaravelとVueを連携したアプリの認証機能の実装が完了しました!

ご指摘、質問等ありましたら、是非コメントお願いします。

読んでいただき、ありがとうございました!

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?