LaravelのAPIに認証を付ける方法としてはPassportやjwt-auth等の外部ライブラリを利用する方法がありますが、「そもそも標準の認証機能って何よ」ということで試してみます。
標準の認証機能
あまり記述が見当たりませんが、認証用のテーブルにapi_tokenってカラムを足して、その値をHTTPヘッダのAuthorization: Bearer の値として送ってやるのが標準?みたいですね(違ってたらご指摘を)。
Guardで下記のように定義されているやつ。
'api' => [
'driver' => 'token',
'provider' => 'users',
],
準備と実装
migrationファイルの編集
では、テーブルを作成します。標準で用意されているmigrationファイルにapi_tokenを追加します。
60文字にしていますが、それより短くても長くてもOKなようです。
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
+ $table->string('api_token',60)->unique()->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}
migrate
migrateします。
php artisan migrate
ユーザーの追加
認証用のユーザー(token)を追加します。
めんどいのでtinkerで追加します。api_tokenには60文字の乱数(乱文字)をセット。
$ php artisan tinker
Psy Shell v0.9.8 (PHP 7.2.7 — cli) by Justin Hileman
>>>
>>>
>>> $user = new App\User;
=> App\User {#2898}
>>> $user->name = 'user1';
=> "user1"
>>> $user->email = 'user1@test.com';
=> "user1@test.com"
>>> $user->password = Hash::make('testtest');
=> "$2y$10$o3Grx73i7PaNV1ptGyukDetQM0FJjxYbXmHdWyNSH0TYK9m2ssf2e"
>>> $user->api_token = str_random(60);
=> "p1zQhxuL2EaNUVUZiMYYPi1iXLWW9R8RLwbMFHjBJmxLgUkkOn1DZApuuobo"
>>> $user->save();
=> true
準備は以上です。サーバを立ち上げておきます。
php artisan serve
動作確認
リクエストしてみる
リクエストもめんどいのでcurlで。
api_tokenに登録した値をAuthorization: Bearer {token}という形で添付して送ります。
APIとしては標準で記述されている認証付きAPIルートである/api/userを利用してみます(リクエストしたユーザの情報を返す)。
API処理としてLaravelに認識させるためにAccept: application/jsonヘッダも追加しています。
curl -H 'Accept: application/json' -H 'Authorization: Bearer p1zQhxuL2EaNUVUZiMYYPi1iXLWW9R8RLwbMFHjBJmxLgUkkOn1DZApuuobo' http://localhost:8000/api/user
レスポンス
あっさりOK。
でも、tokenの内容が見えてるのでいちおう隠します。
{"id":1,"name":"user1","email":"user1@test.com","email_verified_at":null,"created_at":"2018-10-05 22:02:33","updated_at":"2018-10-05 22:02:33","api_token":"p1zQhxuL2EaNUVUZiMYYPi1iXLWW9R8RLwbMFHjBJmxLgUkkOn1DZApuuobo"}
hiddenにapi_tokenを追加
User.phpのhiddenにapi_tokenも追記してやります。
protected $hidden = [
'password', 'remember_token', 'api_token',
];
見えなくなりました。
{"id":1,"name":"user1","email":"user1@test.com","email_verified_at":null,"created_at":"2018-10-05 22:02:33","updated_at":"2018-10-05 22:02:33"}
認証エラーを発生させてみる
tokenの値を少し変えてリクエストしてみます。
ハンドリングされていないエラーが出ます。標準では認証がNGをだとloginに飛ばされるようになっていますが、「loginなんて無いぞ」と言われています。make:authで必要な設定をしてもエラーは消えますが、APIリクエスト認証時のエラーを追加することにします。
{
"message": "Route [login] not defined.",
"exception": "InvalidArgumentException",
.
.
.
とりあえずエラーをでなくする
loginって(名前の)ルートがないぞ!と怒られるので、とりあえずloginって名前のルートを作ります。
めんどいので既にある/にname()で名前つけます。
Route::get('/', function () {
return view('welcome');
})->name('login');
それかエラーの原因となっているリダイレクトを消してもいいです。
protected function redirectTo($request)
{
// return route('login');
}
再度リクエスト
再度(エラーになるよう)リクエストしてみます。API認証エラー時の標準メッセージが出ています。
{"message":"Unauthenticated."}
エラーメッセージを編集する
標準メッセージを変更したい場合はHandle.phpをオーバーライドします。
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
+use Request;
+use Response;
+use Illuminate\Auth\AuthenticationException;
class Handler extends ExceptionHandler
{
protected $dontReport = [
//
];
protected $dontFlash = [
'password',
'password_confirmation',
];
public function report(Exception $exception)
{
parent::report($exception);
}
public function render($request, Exception $exception)
{
return parent::render($request, $exception);
}
//追加
+ public function unauthenticated($request, AuthenticationException $exception)
+ {
+ if($request->expectsJson()){
+ return response()->json(['error' => 'NG'], 401);
+ }
+ return route('login');
+ }
}
一応確認。
{"error":"NG"}
認証の実装より、エラー処理の方が手間。が、とりあえずは以上です。
Passport等の「おまかせ」系に比べ100%自分で制御できてる感があり、これはこれで安心な感じ。