Laravel(Framework 7.10.3を利用)のSanctum(以前はAirlockという名前)で「SPA(Single Page Application)認証」としてのAPIを作成し、フロントエンドはVue.jsを利用してログイン・ログアウト機能を実装してみます。
ログインフォーム画面(/login)と、ユーザー名とメールアドレスを表示させる画面 (/about)を作成します。
ログインしていない状態で/aboutにアクセスすると/loginにリダイレクトされ、ログインしている状態で/loginにアクセスすると/aboutへリダイレクトされるように実装しています。このような画面の切り替え自体はフロント側で実装する必要があり、本記事ではLocalStorageとVue Routerの機能を利用しています。
なおLaravelおよびVue.jsともにローカル環境(localhost)にて構築しています。
プロジェクト作成
1 |
$ composer create-project laravel/laravel sanctum-project --prefer-dist |
MySQLでsample01という名前のデータベースを用意しました。
.env
1 2 3 4 5 6 |
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=sample01 DB_USERNAME=hoge DB_PASSWORD=pass |
次にlaravel/uiパッケージをインストールします。
1 |
$ composer require laravel/ui |
Vue.jsで認証機能の構築に必要なビューなどのファイルが生成されます。
1 |
$ php artisan ui vue --auth |
package.jsonの内容に依存したパッケージおよびvue-routerをインストールします。
1 |
$ npm install && npm install vue-router |
コンパイルを自動化します。
1 |
$ npm run watch |
マイグレーションを実行します。
1 |
$ php artisan migrate |
サーバーを起動させます。
1 |
$ php artisan serve |
tinkerでユーザーデータを登録しておきます。※この時点ではまだブラウザからも普通にユーザー登録ができます。
1 2 |
$ php artisan tinker >>> factory(App\User::class)->create(['name' => 'tarou', 'email' => 'tarou@hoge.co.jp', 'password' => bcrypt('hogehoge')]); |
Sanctumのインストール/設定
Sanctumをインストールします。
1 |
$ composer require laravel/sanctum |
SPAの認証として利用できるように、Kernel.phpファイルのapiミドルウェアにSanctumのミドルウェアを追記します。これでAPIに対するリクエストでセッション・クッキーによる自動認証が可能となります。
app/Http/Kernel.php
1 2 3 4 5 6 7 |
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful; 〜省略 'api' => [ EnsureFrontendRequestsAreStateful::class, 'throttle:60,1', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], |
APIとSPAの構築
本記事ではSanctum以外は基本的に手順のみ記述しています。Vue.jsやSPA自体の説明は下記ページもご覧下さい。
Laravel 6.x〜 Vue.jsを利用する [axios][Laravel Mix]
Laravel Vue.jsのVue RouterでSPA構築
まずはLaravelにおいてAPI用のコントローラーを作成します。
1 |
$ php artisan make:controller LoginController |
下記のように編集します。
app/Http/Controllers/LoginController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?php namespace App\Http\Controllers; 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); } throw ValidationException::withMessages([ 'email' => ['The provided credentials are incorrect'], ]); } public function logout() { Auth::logout(); return response()->json(['message' => 'Logged out'], 200); } } |
api.phpを下記のように編集します。
routes/api.php
1 2 3 4 5 6 7 8 9 10 11 |
<?php use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; Route::middleware('auth:sanctum')->get('/user', function (Request $request) { return $request->user(); }); Route::post('/login', 'LoginController@login'); Route::post('/logout', 'LoginController@logout'); |
6〜8行目 auth:apiをauth:sanctumに変更します。これで認証済の場合にのみリクエストを受け付けるようになります。user()によって認証ユーザーのデータを取得しています。本記事ではabout画面で実際に利用しています。
以上で設定されているルート情報を確認してみます。
1 2 3 4 5 6 7 8 9 10 |
$ php artisan route:list +--------+----------+---------------------+------+------------------------------------------------------------+------------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+---------------------+------+------------------------------------------------------------+------------------+ | | POST | api/login | | App\Http\Controllers\LoginController@login | api | | | POST | api/logout | | App\Http\Controllers\LoginController@logout | api | | | GET|HEAD | api/user | | Closure | api,auth:sanctum | | | GET|HEAD | sanctum/csrf-cookie | | Laravel\Sanctum\Http\Controllers\CsrfCookieController@show | web | | | GET|HEAD | {any} | | Closure | web | +--------+----------+---------------------+------+------------------------------------------------------------+------------------+ |
SPAの土台としてのビュー(テンプレート)を作成します。index.blade.phpをを作成し下記のように記述します。
resources/views/index.blade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!doctype html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="UTF-8"> <title>Sanctumサンプル</title> <meta name="csrf-token" content="{{ csrf_token() }}"> <script src="{{ asset('js/app.js') }}" defer></script> </head> <body> <div id="app"> <router-view /> </div> </body> </html> |
web.phpを下記のように編集します。
routes/web.php
1 2 3 4 5 6 7 |
<?php use Illuminate\Support\Facades\Route; Route::get('{any}', function () { return view('index'); })->where('any', '.*'); |
resources/js/app.jsを下記のように編集します。
1 2 3 4 5 6 7 8 9 10 11 |
require("./bootstrap"); window.Vue = require("vue"); import Vue from "vue"; import router from "./router"; const app = new Vue({ el: "#app", router: router }); |
router.jsファイルをresources/jsディレクトリ以下に作成し、下記のように編集します。
resources/js/router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import Vue from "vue"; import VueRouter from "vue-router"; Vue.use(VueRouter); import login from "./components/login.vue"; import about from "./components/about.vue"; const router = new VueRouter({ mode: "history", routes: [ { path: "/login", name: "login", component: login, meta: { guestOnly: true } }, { path: "/about", name: "about", component: about, meta: { authOnly: true } } ] }); function isLoggedIn() { return localStorage.getItem("auth"); } router.beforeEach((to, from, next) => { if (to.matched.some(record => record.meta.authOnly)) { if (!isLoggedIn()) { next("/login"); } else { next(); } } else if (to.matched.some(record => record.meta.guestOnly)) { if (isLoggedIn()) { next("/about"); } else { next(); } } else { next(); } }); export default router; |
はじめに記述したように、本記事の実装では認証の有無情報はLocalStorageに保存しています。ログイン時にLocalStorageに適当なキー(と値)を用意し、またログアウト時にそのキーを破棄しています(コンポーネントの作成で説明)。そのキーの有無(27行目のisLoggedIn())とVue Routerのルートメタフィールド機能を利用して画面の切り替えをおこなっています。
webpack.mix.jsに下記コードを追記します。
webpack.mix.js
1 2 3 |
mix.js("resources/js/app.js", "public/js") .js("resources/js/router.js", "public/js") .sass("resources/sass/app.scss", "public/css"); |
最後にVue.jsにおける各画面、loginおよびaboutコンポーネントを作成します。
resources/js/components/login.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
<template> <div> <form @submit.prevent="login"> <div> <label>email</label> <input type="text" v-model="email" /> <span v-if="errors.email"> {{ errors.email[0] }} </span> </div> <div> <label>パスワード</label> <input type="password" v-model="password" /> <span v-if="errors.password"> {{ errors.password[0] }} </span> </div> <button>ログイン</button> </form> </div> </template> <script> export default { data() { return { email: "", password: "", errors: [] }; }, methods: { login() { axios.get("/sanctum/csrf-cookie").then(response => { axios .post("/api/login", { email: this.email, password: this.password }) .then(response => { console.log(response); localStorage.setItem("auth", "ture"); this.$router.push("/about"); }) .catch(error => { this.errors = error.response.data.errors; }); }); } } }; </script> |
36行目
ログイン処理の前にCSRF保護を設定しています。ログインのリクエストが成功するとセッション/クッキーによって自動認証されるようになります。
44行目
認証に成功した場合、LocalStorageに適当なキー(と値)を保存します。
resources/js/components/about.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<template> <div> <p>{{ user.name }}</p> <p>{{ user.email }}</p> <button type="button" @click="logout">ログアウト</button> </div> </template> <script> export default { data() { return { user: "" }; }, mounted() { axios.get("/api/user").then(response => { this.user = response.data; }); }, methods: { logout() { axios .post("api/logout") .then(response => { console.log(response); localStorage.removeItem("auth"); this.$router.push("/login"); }) .catch(error => { console.log(error); }); } } }; </script> |
27行目
ログイン時に保存していたLocalStorageを破棄しています。
参照ページ
Vue Router
YouTube QiroLab
SPA Authentication using Laravel Sanctum (formerly Laravel Airlock)