前提条件
フロントエンドとサーバーサイドで別サーバーを立てる場合は同じトップレベルドメインにつ所属している必要があります。
つまりフロントエンドだけ http://127.0.0.1 といった環境では sanctum の認証はうまく動作しません。
token error (The MAC is invalid) となります。
うまくいく例:
サーバーサイド : api.test.com
フロントエンド : local.test.com
Laravel Sanctum には 「1.APIトークン認証」「2. SPA認証(セッション+クッキー)」の2つの認証機能があります。
今回は 2. SPA認証を実装してテストしてみます
composer create-project laravel/laravel my_app
cd my_app
# 以下 sanctum のインストール
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
これにより /api/xxxxx のすべてのURLに認証チェックが入ることになります。
app/Http/Kernel.php:42
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, // ● Laravel Sanctum 追加
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
config/cors.php 次の設定を変更します
'paths' => ['api/*', 'sanctum/csrf-cookie', 'api-register', 'api-login', 'api-logout'], // ● 3つのエンドポイントを追加
'allowed_origins_patterns' => ['/localhost:?[0-9]*/'], // ● localhost:3000 追加
'supports_credentials' => true, // ● trueに変更
config/session.php:158 次の設定を確認します
'domain' => env('SESSION_DOMAIN', null),
.envファイルの SESSION_DOMAIN の値を読み取る設定となっているので、 .env の設定を変更します。
サブドメインに対応するために先頭を . にします
例: dev.myhost.com の場合
.env
# config/session.php の SESSION 設定
SESSION_DOMAIN=".myhost.com"
config/sanctum.php:16 の次の設定を確認します
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
これは次のホスト名をファーストパーティーとして認識させます
localhost
localhost:3000
127.0.0.1
127.0.0.1:8000
::1
env('APP_URL')
開発や実際に動作させるサーバー名がこちらのリスト↑ にない場合は追加するか、 .env の5行目の APP_URL を変更します
app/Http/Controllers/ApiAuthController.php を以下の内容で作成
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Models\User;
use \Symfony\Component\HttpFoundation\Response;
class ApiAuthController extends Controller
{
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required',
'email' => 'required|email',
'password' => 'required'
]);
if ($validator->fails()) {
return response()->json('validation error', Response::HTTP_UNPROCESSABLE_ENTITY);
}
User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
return response()->json('User registration completed', Response::HTTP_OK);
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required'
]);
// ● cookie
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return new JsonResponse(['message' => 'ログインしました']);
}
if ( env('APP_ENV') === 'local' ){
$email = $request->get('email');
$password = $request->get('password');
return response()->json("User Not Found or password don't match. (email:{$email})(password:{$password}) ", Response::HTTP_INTERNAL_SERVER_ERROR);
}
else {
return response()->json("User Not Found or password don't match.", Response::HTTP_INTERNAL_SERVER_ERROR);
}
// ● token
// if (Auth::attempt($credentials)) {
// $user = User::whereEmail($request->email)->first();
// $user->tokens()->delete();
// $token = $user->createToken("login:user{$user->id}")->plainTextToken;
// return response()->json(['token' => $token], Response::HTTP_OK);
// }
// return response()->json("User Not Found or password don't match.", Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
routes/web.php に以下を追加
// (SPAクッキー認証)ユーザー登録 / ログイン / ログアウト
Route::post('/api-register', [\App\Http\Controllers\ApiAuthController::class, 'register']);
Route::post('/api-login' , [\App\Http\Controllers\ApiAuthController::class, 'login']);
Route::post('/api-logout' , [\App\Http\Controllers\ApiAuthController::class, 'logout']);
公開フォルダーに検証用のhtml , js ファイルを置きます。 Vue.js と axios を使用して検証します。
public/test/test-login.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<mycomponent></mycomponent>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js"></script>
<script src="./test-api.js"></script>
<script>
new Vue({
el: '#app'
});
</script>
</body>
</html>
public/test/test-api.js
Vue.component('mycomponent', {
data: function () {
return {
user: {},
userState: 'ログインチェック前'
}
},
mounted: function () {
var self = this;
// ログインチェック実行
self.getUser();
},
methods: {
doLogin: function (event) {
var self = this;
const instance = axios.create({
withCredentials: true
})
const formdata = {};
formdata.email = event.target.elements.email.value;
formdata.password = event.target.elements.password.value;
instance.get('https://YOUR-SERVER.COM/sanctum/csrf-cookie/')
.then(function (response) {
instance.post('https://YOUR-SERVER.COM/api-login', formdata)
.then(function (response) {
console.log('● api-login result');
console.log(response);
alert('ログインしました');
// ログインチェック実行
self.getUser();
})
.catch(function (error) {
alert('api-login エラー');
});
});
},
getUser: function () {
var self = this;
this.userState = '問い合わせ中 ...............';
const instance = axios.create({
withCredentials: true
})
setTimeout(() => {
instance.get('https://YOUR-SERVER.COM/api/user/')
.then(function (response) {
console.log('● ログイン中のユーザー情報');
console.log(response.data);
self.user = response.data;
self.userState = `ログイン中 ( ${response.data.name} / ${response.data.email} )`;
})
.catch(function (error) {
self.userState = '未ログイン';
self.user = {};
});
}, 500);
},
doLogout: function (event) {
var self = this;
const instance = axios.create({
withCredentials: true
})
const formdata = {};
instance.post('https://YOUR-SERVER.COM/api-logout', formdata)
.then(function (response) {
alert('ログアウトしました');
// ログインチェック実行
self.getUser();
})
.catch(function (error) {
alert('api-logout エラー');
});
}
},
template: `
<div>
<form action="/api-login/" @submit.prevent="doLogin">
<h5>ログイン</h5>
<input type="text" name="email" value="">
<input type="text" name="password" value="">
<button type="submit">Vue.jsによるログイン実行</button>
<hr>
<h5>ログインユーザーの取得</h5>
<button type="button" @click="getUser">ユーザー情報取得</button>
<div style="display:inline-block"> → {{userState}}</div>
<div v-if="user.id">
<h5>ログインユーザーのログアウト</h5>
<button type="button" @click="doLogout">ログアウト</button>
</div>
</form>
</div>
`
});
tinker を起動して以下のコードでユーザを作成します。
php artisan tinker
\DB::table("users")->insert([
'name' => 'テストユーザー' ,
'email' => 'test@user.com' ,
'password' => \Hash::make('1234') ,
]);
これで以下の情報でログインすることができます
ID : test@user.com
PASSWORD : 1234
こちらのURLにウェブブラウザでアクセスします
https://YOUR-SERVER.COM/test/test-login.html
Vue.js を通して axios から以下のエンドポイントへxhrを投げます
/sanctum/csrf-cookie/ (getメソッド)(チェック無し) CSRF-TOKEN を暗号化した XSRF-TOKEN を取得します。
/api-login (postメソッド)(1.csrfチェック)ログインの実行
/api-logout (postメソッド)(1.csrfチェック)ログインの実行
/api/user (getメソッド)(1.csrfチェック無し 2.sanctumログインチェック)ログインの実行
/api/ で始まるURLのみ「2.sanctumログインチェック」が入ります。
/api/ 以外のURLのpostメソッドのみ「1.csrfチェック」が入ります。
testディレクトリをローカルマシンの適当なところにダウンロードしてきてそこにExpressサーバーを起動するserver.jsを作成します
server.js ( local.myhost.com )は適宜読み替えてください。
'use strict';
const express = require('express');
const serveIndex = require('serve-index');
const fs = require('fs');
// ==============================サーバ名とポートをセット
const host = 'local.myhost.com';
const port = 3000;
// ==============================サーバ名とポートをセット
const app = express();
const server = require('https').createServer({
key: fs.readFileSync('./privatekey.pem'),
cert: fs.readFileSync('./cert.pem'),
}, app)
app.use(express.static('.'));
app.use(serveIndex('.', {icons: true}));
// app.listen(port, host);
server.listen(port, host, () => console.log(`Server Started https://${host}:${port}`))
openssl req -x509 -newkey rsa:2048 -keyout privatekey.pem -out cert.pem -nodes -days 365
npm init -y
npm i -S express serve-index
node server.js
でローカルサーバーを起動します
こちらからも同様にアクセスができるかどうか検証しましょう。
httpsサーバについてはこちらも参考にしてください
Macのローカルマシンの Express で https:// なサーバを立ち上げて Google Chromeからアクセスする|プログラムメモ
0.0.0.0 local.myhost.com
と書き換えて、ローカルマシンを騙します。 これで https://local.myhost.com:3000 ローカルのマシンにアクセスできます
config/cors.php を確認しましょう
'paths' => ['api/*', 'sanctum/csrf-cookie', 'api-register', 'api-login', 'api-logout'], // ● 3つのエンドポイントを追加
'allowed_origins_patterns' => ['/localhost:?[0-9]*/'], // ● localhost:3000 追加
'supports_credentials' => true, // ● trueに変更
.env を本番環境とlocalhost では切り替える必要があります。
ローカルからアクセスする場合の .env
( myhost.com は適宜読み替えてください)
# config/session.php の SESSION 設定 (サブドメインを除いたドット始まりで記述する。 api.myhost.com の場合 .myhost.com と記述する)
SESSION_DOMAIN=".myhost.com"
# (●local) フロントエンドをローカルマシンにする場合は必ず設定。ドメインとポート番号を記述すること。
# フロントエンドのマシンが https://front.myhost.com:3000 の場合 front.myhost.com:3000 と記述すること
SANCTUM_STATEFUL_DOMAINS=local.myhost.com:3000
# (●local) SESSION_SAME_SITE は次のうちから選択 ("lax", "strict", "none", null ) デフォルトは "lax"
SESSION_SAME_SITE=none
# (●local) http:// なサイトから xhr で送受信するときにCookieをやり取りしたい場合は false をセットしてアンセキュアにする デフォルトは true
# local,本番いずれも 特に変更しなくてデフォルトの true のままで良い
SESSION_SECURE_COOKIE=true