Laravel Passport以外でシンプルな外部サイトからのAPI認証を調べていたら、laravel6.xの公式ページにあった「API認証」がLaravel7.xから無くなっていました。
どうやらLaravel Sanctum(旧Laravel Airlock)を使うようですね。
Laravel Sanctumは2つの認証を提供します。
①APIトークンを生成しAuthorizationヘッダに有効なAPIトークンを含んでいるかで認証するAPI認証。
②トークンを使用せずクッキーベースのセッション認証サービスを利用し、XSSによる認証情報リークに対する保護と同時に、CSRF保護・セッションの認証を提供するSPA認証。
今回はLaravel Sanctumのインストールと設定、そして①のAPI認証を使ってみます。
目次
開発環境
Laradock v10
Laravel 7
Vue 2.6
Vuetify 2.2
Laravel Sanctumのインストールと準備
インストール
Composerでインストールします。
# composer require laravel/sanctum
Configファイルとmigrationファイルの公開
vendorからプロジェクトに必要ファイルを取り込みます。
# php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
実行結果
Copied Directory [/vendor/laravel/sanctum/database/migrations] To [/database/migrations]
Copied File [/vendor/laravel/sanctum/config/sanctum.php] To [/config/sanctum.php]
Publishing complete.
マイグレーションファイルと設定ファイルsanctum.phpができますね。
マイグレーションファイルはこんな感じです。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePersonalAccessTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->bigIncrements('id');
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('personal_access_tokens');
}
}
データベースマイグレーション実行
# php artisan migrate
personal_access_tokensテーブルが生成されます。
APIトークンの発行
Userモデル変更
UserモデルにHasApiTokensトレイトを使用します。
use Laravel\Sanctum\HasApiTokens; // 追加
class User extends Authenticatable
{
use HasApiTokens, Notifiable; // HasApiTokensを追加
}
トークンの発行
$token = $user->createToken('token-name');
平文の値を取得
$token->plainTextToken;
※新規に発行したときのみ平文値を取得できます。
ユーザの全トークンの取得
foreach ($user->tokens as $token) {
// 処理
}
アビリティ(能力)付きトークンの発行
$user->createToken('token-name', ['server:update'])->plainTextToken;
そのトークンが特定のアビリティを持っているか評価するのは以下の通り。
if ($user->tokenCan('server:update')) {
// 処理
}
トークンの破棄
// ユーザの全トークンの破棄
$user->tokens()->delete();
// ユーザの特定トークンの破棄
$user->tokens()->where('id', $id)->delete();
$user->tokens()->where('name', 'token-name')->delete();
ルートの保護
API認証を必要とするルートはsanctum認証ガードを指定する必要があります。
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
CORS対策
クロスオリジンリソースシェアリング(Cross-Origin Resource Sharing)
別ドメインとのリソースの共有ですね。
サーバ側のCORS設定
'supports_credentials' => false,
↓
'supports_credentials' => true,
※Access-Control-Allow-CredentialsヘッダにTrueの値を返します。
クライアント側ではAxiosの設定
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.defaults.withCredentials = true; // 追加
テスト
以上を踏まえて実装してみましょう。
サーバ側
管理画面でトークン作成とAPIでユーザ情報を返すルートの設定
サーバドメイン:alpha.local
ルート設定
管理画面でトークン作成用のルート設定
Route::middleware('verified')->group(function() {
Route::get('/user/sanctum', 'User\SanctumController@index');
Route::post('/user/sanctum/create', 'User\SanctumController@create');
Route::delete('/user/sanctum/destroy', 'User\SanctumController@destroy');
});
APIでユーザ情報を返すルートの設定
Route::middleware('auth:sanctum')->group(function(){
Route::get('/user', function(Request $request){
return $request->user();
});
});
トークン発行/破棄コントローラー作成
# php artisan make:controller User/SanctumController
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class SanctumController extends Controller
{
/**
* Constructor
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}
/**
* Index
*/
public function index()
{
return view('user.sanctum.index');
}
/**
* create
*/
public function create()
{
$user = \Auth::user();
// 古いトークン削除
$user->tokens()->where('name', 'token-name')->delete();
// 新しいトークン生成
$token = $user->createToken('token-name')->plainTextToken;
return redirect('/user/sanctum')->with('token', $token);
}
/**
* destroy
*/
public function destroy()
{
$user = \Auth::user();
// トークン削除
$user->tokens()->where('name', 'token-name')->delete();
return redirect('/user/sanctum')->with('message', 'トークンを破棄しました。');
}
}
トークン発行/破棄ビュー作成
レイアウト
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
<link href="{{ asset('css/user.css') }}" rel="stylesheet">
</head>
<body>
<div id="app">
<main class="py-4">
@yield('content')
</main>
</div>
</body>
</html>
ページ
@extends('layouts.user')
@section('content')
<div class="container">
<h1>Sanctum</h1>
<div class="mb-2">
<form action="/user/sanctum/create" method="post">
@csrf
<button class="btn btn-primary">トークン作成</button>
</form>
</div>
@if (Session::has('token'))
<p class="border mt-1 p-1">{{ Session::get('token') }}</p>
@endif
<div class="mb-2">
<form action="/user/sanctum/destroy" method="post">
@csrf
@method('delete')
<button class="btn btn-danger">トークン破棄</button>
</form>
</div>
@if (Session::has('message'))
<p class="border text-danger mt-1 p-1">{{ Session::get('message') }}</p>
@endif
</div>
@endsection
クライアント作成
APIトークンを利用するリクエストにはAuthrizationヘッダにBearerトークンとして生成されたトークンを含める必要があります。
bearerTokenは直書きしてますが、props等で設定するとして参考程度に。
サーバ:bravo.local
<template>
<div style="max-width: 600px;">
<h1>User</h1>
<table class="table">
<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>
</template>
<script>
const bearerToken = '12|NuGUEhofivihhx3sQPCllRUN6S7YSQDOZLbKWCVdv6VsGUU6UwD4P4oYkIaNY6EXTlWbyCnN3UDcTcU6'
export default {
data() {
return {
user: {
id: '',
name: '',
email: '',
}
}
},
created() {
axios.get('//alpha.local/api/user', {
headers: {
Authorization: `Bearer ${bearerToken}`,
},
})
.then((res) => {
this.user = res.data
})
.catch((err) => {
//
})
},
}
</script>
後記
今回はLaravel Sanctumの2つの認証のうち①APIトークンを生成しAuthorizationヘッダに有効なAPIトークンを含んでいるかで認証するAPI認証を取り扱いました。
何をやっているかわかりやすく、管理画面も簡単に作成できました。これでBearerトークンによる外部API認証は容易に構築できますね。