Laravel 8のAPI認証を味見する - Laravel Sanctum -
はじめに
経緯
Laravel 6で組んでいるAPIサーバーをLaravel 8で組みなおしてみるという一連の実証です。
今回は、Laravel 8でAPI認証を組んでみました。
具体的には、Laravel SanctumでAPI認証の機能が提供されているよ、ということで、これを簡単に使ってみます。
公式ドキュメントのLaravel Sanctumを参考にしています。
Laravel 6で組んでいるAPIサーバーについて補足
Laravel 6のAPI認証は、公式ドキュメントのLaravel 6.x API認証に解説があります。
ドキュメントに、「Laravel Passportの使用を考慮することを強く推奨」というフリのとおり、幾分簡易的なドキュメントという印象です。
というわけで、公式ドキュメントを元に、APIトークンを使い捨てにしたり、有効期限を付与したり、トークンガードをカスタマイズしたりしていました。
Laravel Sanctumでこのあたりを含めどのような使用感になるか見ていきます。
環境
環境は、こちらの記事で作成したものを利用します。
手順概要
以下のとおり進めていきます。
- 準備
DBの準備を行います。 - トークンを発行するコントローラーを設置
認証したユーザーにトークンを発行するためのコントローラーを設置します。 - トークンを発行
認証してトークンを発行します。 - 動作確認
Sanctumにより保護されているルートにアクセスし動作を確認します。
手順
(0)準備
動作確認を行うためのデータベースの準備をします。
Laravelでは認証関連のデータベーステーブルの定義が初めから用意されているため、準備も簡単です。
データベース接続情報の設定
まずはデータベースの接続情報の設定を行います。
Laradockでmysqlコンテナを起動すると、初めからdefaultデータベースが利用可能です。これを使います。
Laravelプロジェクトの.env
を開き、以下の箇所を設定します。(以下は初期設定値に合わせていますが、各自の設定値に変更してくださいませ)
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=default
DB_USERNAME=default
DB_PASSWORD=secret
DB_HOSTは、mysqlコンテナのサービス名であるmysql
を指定することで、MySQLサーバーに接続できます。
その他の設定値は、laradockの.env
のMYSQL_
あたりの設定値と合わせます。
動作確認用データの登録準備
続いて、動作確認用のデータを登録する設定をしておきます。
database/seeders/DatabaseSeeder.php
を開き、コメントアウトされている行を有効にして以下のようにします。
public function run()
{
\App\Models\User::factory(10)->create();
}
データベース準備
そして、テーブルの作成と、動作確認用データの登録です。
これらの作業は、LaravelのArtisanコンソールの機能を利用して実行します。
Artisanコンソールは、Dockerコンテナに接続し、Laravelのプロジェクトディレクトリ内で利用します。
テーブル作成用のコマンドは、migrateです。php artisan migrate
データ登録用のコマンドは、db:seedです。php artisan db:seed
migrateコマンド実行の際に、--seed オプションを指定することで、上記を1コマンドで実行できます。
laradock@d79ca8e4255f:/var/www/example-app01$ php artisan migrate --seed
実行結果イメージ
laradock@d79ca8e4255f:/var/www/example-app01$ php artisan migrate --seed
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (36.49ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table (24.43ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated: 2019_08_19_000000_create_failed_jobs_table (25.34ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated: 2019_12_14_000001_create_personal_access_tokens_table (35.36ms)
Database seeding completed successfully.
補足:コンテナ起動とコンテナ接続
作業はコンテナ内で行う、というわけで、あらかじめコンテナ起動やらコンテナ接続やらが必要です、念のため補足として記載。
workspace\laradock> docker-compose up -d nginx mysql phpmyadmin
Creating network "laradock_frontend" with driver "bridge"
Creating network "laradock_backend" with driver "bridge"
Creating network "laradock_default" with the default driver
Creating laradock_mysql_1 ... done
Creating laradock_docker-in-docker_1 ... done
Creating laradock_workspace_1 ... done
Creating laradock_phpmyadmin_1 ... done
Creating laradock_php-fpm_1 ... done
Creating laradock_nginx_1 ... done
workspace\laradock> docker-compose exec --user=laradock workspace bash
laradock@d79ca8e4255f:/var/www$ cd example-app01/
laradock@d79ca8e4255f:/var/www/example-app01$
※ついでにphpmyadminを起動(http://localhost:8081/
でアクセス可能)
参考:migrateの再実行
migrateコマンドは世代管理されています。そのため、再実行してもmigrationsに変更がなければ以下のように何も行われません。
laradock@d79ca8e4255f:/var/www/example-app01$ php artisan migrate --seed
Nothing to migrate.
Database seeding completed successfully.
また、上記の場合、seedだけは行われるため、初期データが2重登録されるため注意です。
migrateコマンドは、以下のようにfreshを指定すると、初期化して再実行できます。
laradock@d79ca8e4255f:/var/www/example-app01$ php artisan migrate:fresh --seed
migrate:freshは開発環境を再構築する場合によく使いますが、本番環境で基本的に使用しません。
そのあたりを意識するならば、以下のように、一度一つ前のマイグレーションに戻してから再度実行する、というやり方をするのがよいかもしれません。
laradock@d79ca8e4255f:/var/www/example-app01$ php artisan migrate:rollback
Rolling back: 2019_12_14_000001_create_personal_access_tokens_table
Rolled back: 2019_12_14_000001_create_personal_access_tokens_table (17.39ms)
Rolling back: 2019_08_19_000000_create_failed_jobs_table
Rolled back: 2019_08_19_000000_create_failed_jobs_table (9.67ms)
Rolling back: 2014_10_12_100000_create_password_resets_table
Rolled back: 2014_10_12_100000_create_password_resets_table (12.28ms)
Rolling back: 2014_10_12_000000_create_users_table
Rolled back: 2014_10_12_000000_create_users_table (10.97ms)
laradock@d79ca8e4255f:/var/www/example-app01$ php artisan migrate --seed
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (32.09ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table (23.66ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated: 2019_08_19_000000_create_failed_jobs_table (26.95ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated: 2019_12_14_000001_create_personal_access_tokens_table (34.58ms)
Database seeding completed successfully.
マイグレーションの状況を確認するコマンドもあります。
laradock@d79ca8e4255f:/var/www/example-app01$ php artisan migrate:status
+------+-------------------------------------------------------+-------+
| Ran? | Migration | Batch |
+------+-------------------------------------------------------+-------+
| Yes | 2014_10_12_000000_create_users_table | 1 |
| Yes | 2014_10_12_100000_create_password_resets_table | 1 |
| Yes | 2019_08_19_000000_create_failed_jobs_table | 1 |
| Yes | 2019_12_14_000001_create_personal_access_tokens_table | 1 |
+------+-------------------------------------------------------+-------+
(1)トークンを発行するコントローラーを設置
APIトークンは、認証したユーザーに対して発行します。そのため、認証処理が必要です。
これを実現するため、認証用のコントローラーを設置します。
コントローラーはArtisanのコマンドで作成できます。
php artisan make:controller Api/AuthController
※コントローラーについては、公式ドキュメントのLaravel 8.x コントローラへ。
作成したコントローラーで、通常の認証を行い、その流れでAPIトークンの発行処理まで行いAPIトークンを返却します。
参考:Laravel 通常認証について
Laravelにおける通常認証は、Laravel 8.x 認証に記載がありますが、Laravelには初めから認証機能が組み込まれているため、簡単に実装できます。
APIトークンの発行処理は、Laravel Sanctum - APIトークンの発行に記載のとおりです。
というわけで、作成した AuthController の内容を以下のようにします。
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
public function authenticate(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (Auth::attempt($credentials)) {
$token = $request->user()->createToken('token-name');
return response()->json(['api_token' => $token->plainTextToken], 200);
}
return response()->json(['api_token' => null], 401);
}
}
設置したコントローラーにアクセスできるようにするため、ルート設定を行います。
routes/api.php
に以下を追記します。
- 先頭の方にuse宣言として以下を追加
use App\Http\Controllers\Api\AuthController;
- 最後の方に以下を追加
Route::post('/authenticate', [AuthController::class, 'authenticate']);
これで、認証の上、APIトークンを発行できるようになりました。
(2)トークンを発行
RESTクライアントで先ほどのルート設定にアクセスします。
クライアントは、Insomniaを使います。
-
Insomniaの適当なワークスペースを開きます。
-
New Request
をクリック(Ctrl + N を押してもOK)して新しいリクエストを作成します。- Nameは
Auth
など、適当な名前を入力 - Name入力欄の右のメソッドに
POST
を選択
- Nameは
-
POSTのURIに以下を入力します。
http://localhost/app01/api/authenticate
※環境に合わせて調整 -
「Body ▼」をクリックし、「JSON」を選択します。
-
JSONのデータに以下のようにデータを入力します。
emailはDBのUserテーブルの値に置き換えてください。
Factoryで作成したユーザーの場合、パスワードはpassword
です。{ "email": "szboncak@example.com", "password": "password" }
Userデータの調べ方
phpMyAdminで確認するか、tinkerを使ってデータを確認することもできます。
laradock@d79ca8e4255f:/var/www/example-app01$ php artisan tinker --execute='dump(App\Models\User::find(1)->email)' "szboncak@example.com"
-
Headerタブをクリックし、以下を入力します。
- 1つ目
Header: Accept
Value: application/json - 2つ目
Header: Content-Type
Value: application/json
ヘッダ内容
設定したヘッダの内容は以下のイメージです。
Accept: application/json Content-Type: application/json
- 1つ目
-
Sendボタンをクリックしリクエストを投げます。
リクエストが送信され、サーバー側で正常に処理が行われると以下のようなレスポンスデータを受け取れます。{ "api_token": "1|LQSoLxvnoq42lqL0rcnSrPTvTFO0eM28xnpPuhbF" }
このapi_tokenの値を
Authorization
ヘッダのBearerトークンとして設定しリクエストを投げることで、API認証を通過することができます。補足:先頭の数字は何
パイプ(|)で区切られた左の数値はレコードIDでした。
右の文字列が、実際のトークン値です。
Models\Sanctum\PersonalAccessToken::findToken
の処理を見ると、レコードIDを含めても省略してもどちらでも問題ないようになっています。
レコードIDを含めることで、テーブルから該当レコードをレコードIDで検索することになり、パフォーマンスがあがる、ということのようです。
(3)動作確認
動作確認1:401 Unauthorized
動作確認をしてみます。
routes/api.php
には、初めからsanctumの保護を受けたルート設定があるため、こちらを使用します。
まずは、APIトークンを含めないリクエストを投げてみます。
Insomniaで、新しいリクエストをGET
で作成し、以下のURIを設定します。
http://localhost/app01/api/user
Headerタブをクリックし、トークン発行の時と同じように、Accept
ヘッダーと、Content-Type
ヘッダーにapplication/json
をセットします。
この設定でSendボタンを押すと、401 Unauthorized
が返され、以下のデータが返却されます。
{
"message": "Unauthenticated."
}
これが、Sanctumにより保護されたルートに、未認証でアクセスした際の挙動です。
動作確認2:API認証
発行したAPIトークンをセットして、保護されたルートにアクセスしてみます。
先ほどのリクエストに以下のヘッダーを追加します。
Header: Authorization
Value: Bearer 1|LQSoLxvnoq42lqL0rcnSrPTvTFO0eM28xnpPuhbF
Valueとして、先頭にBearer
を付加して発行されたAPIトークン(api_token
の値)を指定します。
ヘッダ内容
設定したヘッダの内容は以下のイメージです。
Accept: application/json
Content-Type: application/json
Authorization: Bearer 1|LQSoLxvnoq42lqL0rcnSrPTvTFO0eM28xnpPuhbF
この設定でSendボタンを押すと、API認証が行われた上で、以下のようなレスポンスデータが返却されます。
{
"id": 1,
"name": "Jonatan Lueilwitz",
"email": "szboncak@example.com",
"email_verified_at": "2021-09-22T00:00:00.000000Z",
"created_at": "2021-09-22T00:00:00.000000Z",
"updated_at": "2021-09-22T00:00:00.000000Z",
}
こちらが、Sanctumにより保護されたルートに対し、APIトークンを用いた認証を行ってレスポンスを受け取った際の挙動です。
Laravel Sanctum によるAPI認証の基本的な挙動は以上です。
おまけ
Insomniaでは、ヘッダーの設定の右側にチェックボックスがあり、ヘッダーの有効・無効を簡単に切り替えられるようになっています。
Authorization
ヘッダーや他のヘッダーのチェックボックスをオン・オフしたり、メソッドをGETからPOSTに変えてみたり、と、いろいろと試してみると良いでしょう。
Insomniaの環境変数
リクエスト設定はAPIごとに作成しますので、都度AuthorizationヘッダーにBearerトークンを設定することになります。
このときBearerトークンに直接APIトークンを埋め込むと、APIトークンが無効になった場合にそれぞれのAPIトークンを更新することになり、不便です。
Insomniaでは、環境変数を設定することができますので、APIトークンを環境変数に登録しておくことでこの問題を回避できます。
環境変数は、「No Environment ▼」をクリックし、「Manage Environments」をクリックして表示されるダイアログで設定できます。(Ctrl + E を押すことでもダイアログを表示できます。)
Base Environmentに以下のように入力するとTOKEN
という変数に値を設定できます。
{
"TOKEN": "1|LQSoLxvnoq42lqL0rcnSrPTvTFO0eM28xnpPuhbF"
}
環境変数を参照するには、値の入力欄で_
を入力します。すると環境変数の選択メニューが表示されるので、ここから該当の変数を選択します。
選択すると、色付きで_.TOKEN
のように変数が表示されます。これをクリックすると直接値の編集もできます。
考察
様々な補足を入れているため説明が多くなりましたが、実際の動作確認としてはシンプルなものでした。
Laravel 6のやり方もシンプルでしたが、比べるとさらにシンプルになっている(というか完全に標準化されている)印象があります。
ただし、SanctumのAPI認証は、認証されたユーザーが利用するAPIトークンを生成し、サードパーティ的なサービスとアプリケーション連携を行う、といった利用方法を想定した仕組みのようです。
そのため、APIトークンに名前(メモ的な)をつけられたり、アビリティを設定することで認可の仕組みが実装できるなどの考慮がなされています。
一方で、個人的にやりたいこと(Laravel 6でやっていたこと)は、1回の認証の最中にのみ有効なAPIトークンを1つだけ発行する、というものです。
このような使い方ができないという訳ではないですが、想定とは異なる使い方をすると捉え、想定の仕組みをしっかり理解した上で拡張をすることが必要となるな、という感想です。
ちなみに、他の記事に記載したVuetifyのようなSPAでAPI通信する場合、Laravel Sanctumではクッキーベースのセッション認証サービスを利用できるようです。詳しくはSPA認証を参照のこと。
併用もできそうなので、このあたりも検討の余地がありそうです。
というわけで今回はここまで。おつかれさまでした。
Discussion