前回は「Laravel SanctumでBearerトークンによるAPI認証」を行いましたが、今回はLaravel Sanctumのもうひとつの認証、トークンを使用せずクッキーベースのセッション認証であるSPA認証を行います。
「Laravel SanctumでBearerトークンによるAPI認証」に追加設定することになりますのでLaravel SanctumのインストールやUserモデルの設定、トークンの発行はご参照願います。
目次
開発環境
Laradock v10
Laravel 7
Vue 2.6
Vuetify 2.2
SPA認証用のSanctumのミドルウェア追加
SPA認証用にapp/Http/Kernel.phpのapiミドルウェアグループにSanctumのミドルウェアを追加します。
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful; // 追加
'api' => [
EnsureFrontendRequestsAreStateful::class, // 追加
'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
-----
CORS対策に追加
'paths' => ['api/*'],
↓
'paths' => ['api/*', 'sanctum/csrf-cookie'], // 変更
.env設定
Laravel Sanctumが対応するドメインを設定します。
ローカルでは動作しても外部サーバで動作しないときはチェックしましょう!
SANCTUM_STATEFUL_DOMAINS=****.***
※ドメインがlocalhostでない場合、ドメイン名を設定します。カンマ区切りで複数設定できるみたいですね。
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1,127.0.0.1:8000,::1')),
認証
/sanctum/csrf-cookieルートへのリクエストを作成し、アプリケーションのCSRF保護を初期化してからログイン処理を実行しなければなりません。
axios.get('/sanctum/csrf-cookie').then(response => {
// ログイン処理…
});
ログイン後はAPIルートに対する認証は自動的に行われます。
Route::middleware('auth:sanctum')->group(function(){
//
});
テスト
以上を踏まえて実装してみましょう。
ログインコントローラー
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
/**
* ログイン
*
* @param \Illuminate\Http\Request $request
*
* @return Response
*/
public function login(Request $request)
{
$result = false;
$status = 401;
$message = 'ユーザが見つかりません。';
$user = null;
$credentials = $request->only('email', 'password');
if (Auth::attempt($credentials)) {
// Success
$result = true;
$status = 200;
$message = 'OK';
$user = Auth::user();
// ※古いトークン削除&新しいトークン生成
$user->tokens()->where('name', 'token-name')->delete();
$token = $user->createToken('token-name')->plainTextToken;
}
return response()->json(['result' => $result, 'status' => $status, 'message' => $message]);
}
/**
* ログアウト
*
* @return Response
*/
public function logout()
{
Auth::logout();
$result = true;
$status = 200;
$message = 'ログアウトしました。';
return response()->json(['result' => $result, 'status' => $status, 'message' => $message]);
}
}
「※古いトークン削除&新しいトークン生成」は、すでにトークンが存在していれば必要ありません。
APIルーティング
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/login', 'Api\LoginController@login');
Route::get('/logout', 'Api\LoginController@logout');
Route::middleware('auth:sanctum')->group(function(){
Route::get('/user', function(Request $request){
return $request->user();
});
});
Vue-router
import Vue from 'vue'
import Router from 'vue-router'
import SpaLogin from './components/SpaLoginComponent.vue'
import SpaUser from './components/SpaUserComponent.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/spalogin',
name: 'SpaLogin',
component: SpaLogin,
},
{
path: '/spauser',
name: 'SpaUser',
component: SpaUser,
},
],
})
Vueコンポーネント
<template>
<div class="m-auto" style="width: 320px;">
<h1>SPA Login</h1>
<div>
<v-form
ref="form"
v-model="valid"
>
<v-text-field
v-model="email"
:rules="[rules.required]"
label="E-MAIL"
></v-text-field>
<v-text-field
v-model="password"
:rules="[rules.required]"
:append-icon="passwordShow ? 'mdi-eye' : 'mdi-eye-off'"
:type="passwordShow ? 'text' : 'password'"
name="password"
label="Password"
@click:append="passwordShow = !passwordShow"
></v-text-field>
<v-btn :disabled="!valid" color="primary" @click="login">LOGIN</v-btn>
<v-btn color="red" @click="logout">LOGOUT</v-btn>
</v-form>
<p class="mt-2 text-danger">{{ loginMessage }}</p>
</div>
<div>
<v-btn color="success" class="mt-2" @click="getUser">GetUser</v-btn>
<p class="mt-2 text-danger">{{ getUserMessage }}</p>
<table class="table mt-2">
<tr>
<th>ID</th>
<td>{{ user.id }}</td>
</tr>
<tr>
<th>Name</th>
<td>{{ user.name }}</td>
</tr>
<tr>
<th>E-Mail</th>
<td>{{ user.email }}</td>
</tr>
</table>
</div>
<router-link to="/SpaUser">
<v-btn color="warning" class="mt-2">SPA User</v-btn>
</router-link>
</div>
</template>
<script>
export default {
data() {
return {
valid: true,
isLogin: false,
email: '',
password: '',
passwordShow: false,
loginMessage: '',
user: {},
getUserMessage : '',
rules: {
required: value => !!value || 'Required.',
}
}
},
created() {
// this.logout()
},
methods: {
login() {
axios.get('/sanctum/csrf-cookie')
.then((res) => {
axios.post('/api/login', {
email: this.email,
password: this.password,
})
.then((res) => {
console.log(res.data)
if (res.data.result) {
this.isLogin = true
this.getUserMessage = ''
} else {
this.loginMessage = res.data.message
}
})
.catch((err) => {
console.log(err)
})
})
.catch((err) => {
//
})
},
logout() {
axios.get('/api/logout')
.then((res) => {
this.user = {}
this.isLogin = false
})
.catch((err) => {
console.log(err)
})
},
getUser() {
this.user = {}
this.getUserMessage = ''
axios.get('/api/user')
.then((res) => {
this.user = res.data
})
.catch((err) => {
console.log(err)
this.getUserMessage = '取得できません。'
})
},
},
}
</script>
<template>
<div class="m-auto" style="width: 320px;">
<h1>SPA User</h1>
<div>
<router-link to="/SpaLogin">
<v-btn color="primary" class="mt-2">SPA Login</v-btn>
</router-link>
<p class="mt-2 text-danger">{{ getUserMessage }}</p>
<table class="table mt-2">
<tr>
<th>ID</th>
<td>{{ user.id }}</td>
</tr>
<tr>
<th>Name</th>
<td>{{ user.name }}</td>
</tr>
<tr>
<th>E-Mail</th>
<td>{{ user.email }}</td>
</tr>
</table>
</div>
</div>
</template>
<script>
export default {
data() {
return {
user: {},
getUserMessage : '',
}
},
created() {
this.getUser()
},
methods: {
getUser() {
this.user = {}
this.getUserMessage = ''
axios.get('/api/user')
.then((res) => {
this.user = res.data
})
.catch((err) => {
console.log(err)
this.getUserMessage = '取得できません。'
})
},
},
}
</script>
後記
Laravel SanctumでのSPA認証はCookieベースのセッション認証なのでリロードされても大丈夫ですね!
有効期限はconfig/sanctum.phpのexpirationで設定(分単位)できるようです。
'expiration' => null,