Laravel SanctumでVue.jsの認証を実装する

Laravel SanctumでVue.jsの認証を実装する

フロントをVue.js、バックエンドをLaravel(API)として構成する場合のLaravel/Sanctumの構成方法です。

Laravel/Sanctumとは

公式で推奨されているSPA向けの認証パッケージですが、如何せんLaravelの中の人はJetStream押しが強く詳細なリファレンスが用意されていません。
JetStreamを使えばパッとスキャフォールディングで出来ますよ、とPRするのは良いけれど漏れなくinertiaが付いてきたり、知りたいのはVuexやrouter、axiosで実装する方法です。

プロジェクト用ルートディレクトリを作成する

Laravel内でVue.jsを使用せず、Server, Clientで分けて極力依存しないように構成にしたいと思います。
また、Docker等も理解していれば便利なので使いたくなりますが、今回は本題ではないので余計なものは使わず進めます。

mkdir sanctum_example

Laravelをインストールする

Laravelをcurlからインストールします。
URLのserverの部分がディレクトリ名になるので任意で変更します。

cd sanctum_example
curl -s https://laravel.build/server | bash

Vueをインストールする

Vue CLIをインストールする

VueをコマンドラインからインストールするためにCLIをインストールします。
Node.jsをインストールしていなければ事前にインストールしておきます。

npm install -g @vue/cli

Vue.jsをインストール

対話式で構成を決めていきます。
認証状態をアプリケーション全体で保持するために、vuex, vue routerを選択してインストールします。

vue create client

Laravel/Sanctumをインストール

serverディレクトリに移動してからcomposerからLaravel/Sanctumをインストールします。
composerをインストールしていなければ事前にインストールしておきます。

composer require laravel/sanctum

設定ファイルとマイグレーションファイルを追加

sanctum設定ファイルを追加し、APIトークンを保存するフィールドを作成するマイグレーションファイルを追加します。

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

マイグレーションを実行します。

php artisan migrate

SPA認証 クッキーベースの認証

Sanctumを使ったSPAの認証にはトークンは使用せず、クッキーベースの認証が推奨されています。

Sanctumミドルウェアを追加する

server/app/Http/Kernel.php

'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

CORSを設定する

異なるドメイン間で使用する場合は、supports_credentialstrueに設定します。

server/config/cors.php

'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,

LoginControllerを追加する

スキャフォールディングは用意されていないため、Laravelの標準の認証に従って認証機能を実装していきます。

server/app/Http/Controllers/Auth/LoginController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class LoginController extends Controller
{
    public function login(Request $request)
    {
        $credentials = $request->validate([
            'email' => 'required|email',
            'password' => 'required'
        ]);

        if (Auth::attempt($credentials)) {
            return response()->json(['message' => 'Login successful'], 200);
        }

        return response()->json(['message' => 'User not found'], 422);
    }

    public function logout()
    {
        Auth::logout();
        return response()->json(['message' => 'Logged out'], 200);
    }
}

ルーティングを追加する

ログイン・ログアウトのルーティングを追加します。

server/routes/api.php

Route::prefix('auth')->group(function () {
    Route::post('/login', [App\Http\Controllers\Auth\LoginController::class, 'login']);
    Route::post('/logout', [App\Http\Controllers\Auth\LoginController::class, 'logout']);
});

クライアント側(Vue.js)のログイン機能を追加する

storeを定義

vuexで認証状態・認証ユーザー情報を保持するようにします。

client/src/store/auth.js

import axios from 'axios';
export default {
  namespaced: true,
  state: {
    isAuth: false,
    user: null,
  },
  getters: {
    isAuth(state) {
      return state.isAuth;
    },
    user(state) {
      return state.user;
    },
  },
  mutations: {
    SET_IS_AUTH(state, value) {
      state.isAuth = value;
    },
    SET_USER(state, value) {
      state.user = value;
    },
  },
  actions: {
    async login({ dispatch }, credentials) {
      await axios.get('/sanctum/csrf-cookie');
      await axios.post('/api/auth/login', credentials);
      return await dispatch('me');
    },
    async me({ commit }) {
      return await axios
        .get('/api/user')
        .then(response => {
          commit('SET_IS_AUTH', true);
          commit('SET_USER', response.data);
        })
        .catch(() => {
          commit('SET_IS_AUTH', false);
          commit('SET_USER', null);
        });
    },
  },
};

Loginページを作成する

ログインページを作成します。

client/src/views/Login.vue

<template>
  <div>
    <form @submit.prevent="submit">
      <div>
        <label>Eメール</label>
        <input type="text" v-model="form.email" />
      </div>
      <div>
        <label>パスワード</label>
        <input type="password" v-model="form.password" />
      </div>
      <button type="submit">ログイン</button>
    </form>
  </div>
</template>
<script>
import { mapActions } from 'vuex';
export default {
  data() {
    return {
      form: {
        email: '',
        password: '',
      },
    };
  },
  methods: {
    ...mapActions({
      login: 'auth/login',
    }),
    async submit() {
      await this.login(this.form);
      this.$router.replace({ name: 'Home' });
    },
  },
};
</script>

ルーティングを定義

/loginにリクエストを受けたら、Loginページへ遷移するように定義します。
Homeページは認証されていなければLoginページへリダイレクトするようにbeforeEachで設定します。

client/src/router/index.js

import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';
import Login from '@/views/Login.vue';

import Store from '@/store/index.js';

Vue.use(VueRouter);

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: Login,
  },
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: {
      isAuthenticated: true,
    },
  },
];

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.isAuthenticated)) {
    if (!Store.state.auth.isAuth) {
      next({ name: 'Login' });
    } else {
      next();
    }
  }
  next();
});

export default router;

認証テスト用ユーザーを作成する

tinkerを使って直接コマンドラインから追加します。

php artisan tinker
\App\Models\User::factory()->create(['name' => 'genba neko', 'email' => 'neko@neko.jp', 'password' => bcrypt('neko')]);

確認

npm run serveからVueを実行して/loginページを表示します。
認証テスト用ユーザーの認証情報を入力して、認証に成功するか確認しましょう。

よくある問題

Clientが別のドメイン・ポート

CORSによって別ドメインからアクセスする場合は、Laravel側の.envで事前に設定が必要です。

SANCTUM_STATEFUL_DOMAINS=localhost:8080

プログラミングカテゴリの最新記事