3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

bravesoftAdvent Calendar 2024

Day 9

【Laravel11】Sanctumでusers以外のテーブルでもAPI認証がすぐできる方法

Last updated at Posted at 2024-12-09

みなさま、お疲れ様です。
フロント、サーバーサイドなどWEBでいろんなことをさせていただいております、エンジニアのkuharaです。

3年前にもアドベントカレンダーがあり、そのときは弊社のブログでしたが、
このような記事も書かせていただき、(ブログのシステムの不具合でなければ)不動の1位となっております。自分でもこうなるとは思っていませんでした。
ご覧いただきましたみなさまに熱く御礼を申し上げます。
https://bravesoft.co.jp/blog/archives/15492/

さて、最近すごく忙しく、今回のAdventCalendarもこっそりリスケしておりました。
そのような忙しい間でさえもサクッと、APIの認証が実装できちゃう方法を見つけて実践しましたので、今回のネタとさせていただきます。

前提

  • DB環境があること(今回はMySQL)
  • データベースの作成が済んでいること
  • Laravelコマンドが実行できること

今回やること

モバイルアプリ向けにSanctumを使って、デフォルトとして準備されているusers以外のテーブルでもAPI認証をトークンで行えるようにします。
ログイン情報をusers以外のテーブルに格納し、それを元に認証ができるようになります。

背景

案件の情報になるのでほとんどが秘密事項ですが、例えば
・usersという名前が嫌だ
・usersテーブルを他の認証(例えばSPA、Bladeなど)に使う予定
といった方におすすめできるかと思います。

目次

  • Laravel環境インストール
  • Sanctumインストール
  • マイグレーション作成・実行
  • コード書き換え
    • app/Http/Controllers/使うController.php
    • app/Models/認証に使いたいテーブルのModel.php
    • config/auth.php
  • Middlewareの作成
  • DBにアカウント情報を登録
  • POSTMANでいざ、実践

Laravel環境インストール

$ laravel new your-pj
$ cd your-pj
この後で、.envの設定を行い、ご自分で作られたものに設定しましょう。
$ php artisan tinkerで、DB::select("select 1")でエラーが出なければOKです。
your-pjはお好きなお名前

Sanctumインストール

$ php artisan install:api
これを実行しSanctumをインストールしましょう。

マイグレーションするかどうか聞かれると思いますが、ここではまだやらなくてもいいです。

マイグレーション作成・実行

今回、メールアドレスなどは無しの、ただIDとパスワードだけの超シンプルなものを実装します。

database/migrations/0001_01_01_000000_create_persons_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('persons', function (Blueprint $table) {
            $table->id();
            $table->string('account_id');
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('persons');
    }
};

スクリーンショット 2024-12-07 22.01.19.png

もともと、database/migrations/0001_01_01_000000_create_users_table.phpが存在しましたが、これを自分で設定したいテーブル名に変えてしまいます。ここでは"persons"とします。

なお、password_reset_tokensやsessionsも、API認証ではいらないので削除します。

カラムの設定は、

5つの設定項目
$table->id();
$table->string('account_id');
$table->string('password');
$table->rememberToken();
$table->timestamps();

これで十分です!

また、Sanctumをインストールしたときに以下のファイルも作成されます。このまま利用します。

database/migrations/2024_12_09_000000_create_personal_access_tokens_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('personal_access_tokens', function (Blueprint $table) {
            $table->id();
            $table->morphs('tokenable');
            $table->string('name');
            $table->string('token', 64)->unique();
            $table->text('abilities')->nullable();
            $table->timestamp('last_used_at')->nullable();
            $table->timestamp('expires_at')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('personal_access_tokens');
    }
};
  • database/migrations/0001_01_01_000000_create_users_table.php
  • database/migrations/2024_12_09_000000_create_personal_access_tokens_table.php

この2つのファイルを今回マイグレーションしましょう。
$ php artisan migrate

コード書き換え

あとは、これをコピペでやっていきましょう!

最初に整理しておくと、

  • 書き換えるべきファイル

    • database/migrations/0001_01_01_000000_create_persons_table.php (これは前章で書き換えたのでここでは触れません。)
    • app/Http/Controllers/使うController.php
    • app/Models/認証に使いたいテーブルのModel.php
    • config/auth.php
    • app/Http/Middleware/EnsureTokenIsValid.php(Middelware作成で触れます)
    • routes/api.php(Middelware作成で触れます)
  • 新規でインストールor書き換えされるが変更しないファイル

    • database/migrations/2024_12_09_000000_create_personal_access_tokens_table.php
    • config/sanctum.php(何も書き換えずにコミットしてください)
    • bootstrap/app.php (sanctumのインストール時、withRoutingにapi: __DIR__.'/../routes/api.php',が挿入されます。)
app/models/Person.php
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; //【1】

class Person extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasApiTokens, HasFactory, Notifiable; //【2】

    protected $table = 'persons'; //【3】

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'account_id',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];
}

スクリーンショット 2024-12-07 22.07.59.png
デフォルトで入っていたUser.phpを書き直したものですが、ポイントとして、
【1】 use Laravel\Sanctum\HasApiTokens;をインポート
【2】クラス内のuseにHasApiTokensを追加
【3】使用するテーブル名を指定
あと、肝心のクラス名も使用するテーブルにあわせておきましょう。

casts()は・・・・ここではいらないので削除します。

config/auth.php

config/auth.php
<?php

return [
    'defaults' => [
        'guard' => env('AUTH_GUARD', 'api'), //【1】
        'passwords' => env('AUTH_PASSWORD_BROKER', 'persons'), //【2】
    ],

    'guards' => [
        'api' => [
            'driver' => 'sanctum',//【3】
            'provider' => 'persons',//【4】
        ],
    ],

    'providers' => [
        'persons' => [ //【5】
            'driver' => 'eloquent',
            'model' => env('AUTH_MODEL', App\Models\Person::class),//【6】
        ],
    ],
];

【1】今回APIなので、'api'とします。
【2】personsテーブルを使うので、'persons'にしましょう。
【3】Sanctumを使うので、'sanctum'にしましょう。
【4】ここと、【5】での指定が一致していれば、'persons'でなくても'hoge'でも'foo'でもいいです。
【6】personsテーブルが紐づいているのはPersonモデルになるので、ここでそれを設定します。

app/Http/Controllers/使いたいController.php

app/Http/Controllers/APIController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use App\Models\Person;

class APIController extends Controller
{
    public function login(Request $request)
    {
        // `account_id` でユーザーを検索します
        $user = Person::where('account_id', $request->input('account_id'))->first();

        if (!$user || !Hash::check($request->input('password'), $user->password)) {
            return response()->json([
                "message" => "IDまたはパスワードが間違っています。"
            ], 401);
        }
        // トークンを発行します
        $token = $user->createToken('api-token')->plainTextToken;

        return response()->json([
            "message" => "ログインしました",
            "token" => $token
        ]);
    }

    public function logout(Request $request)
    {
        $request->user()->tokens()->delete();

        return response()->json(["message" => "ログアウトしました"]);
    }

    public function person(Request $request)
    {
        return response()->json(Auth::user());
    }
}

Middlewareの作成

ここで、トークンが無効であったときの処理をつくっておきます。

app/Http/Middleware/EnsureTokenIsValid.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class EnsureTokenIsValid
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function handle(Request $request, Closure $next)
    {
        if (!Auth::guard('sanctum')->check()) {
            return response()->json(['message' => 'トークンが間違っています。'], 401);
        }

        return $next($request);
    }
}

そして、そのルーティングにおいてミドルウェアを使う範囲を指定しておきます。
Sanctumはもう上記ミドルウェアに入っているため、ここで指定するミドルウェアはEnsureTokenIsValidでいいです。
今回login以外は全てトークンがなければ使えない機能になるので、login以外を指定します。

routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\APIController; //使いたいコントローラ

use App\Http\Middleware\EnsureTokenIsValid;

Route::post('/login', [APIController::class, 'login']);

Route::middleware([EnsureTokenIsValid::class])->group(function () {
    Route::post('/logout', [APIController::class, 'logout']);
    Route::get('/person', [APIController::class, 'person']);
});

DBにアカウント情報を登録

パスワードはtinkerでHash::make("password");で得られたものをpersonsテーブルのpasswordカラムに登録します。IDとして使うaccount_idには、お好きなものをどうぞ!

INSERT INTO persons (account_id, password) VALUES ('ID0001', '$2y$12$GKjBCgslNSYjtKn1NugqG.M7auvoZD8/Rk7xwLB4K0niFQHyQnXIm');

POSTMANでいざ、実践!

※作り終えた後ですが、
$ php artisan key:generate
忘れないようにしましょう。

ローカルで実験するには、
$ php artisan serve --port=9876
でサーバーを立ち上げましょう。ここでは実験なのでポートはお好みで。

まずはログインを試します。

スクリーンショット 2024-12-08 22.10.03.png
パスワードが間違っていれば失敗しますが、

スクリーンショット 2024-12-08 22.11.21.png
あっていればトークンが発行されます。

スクリーンショット 2024-12-08 22.11.55.png
そのトークンを、次はBearer Tokenに貼り付けて実験します。あえてトークンを間違ってみると、EnsureTokenIsValidで書いた処理が発動します。

スクリーンショット 2024-12-08 22.12.06.png
正しいトークンなら成功しますね!できました!

ログアウトもきちんとできました!スクリーンショット 2024-12-08 22.12.29.png

おわりに

いかがでしたでしょうか?
認証周りは結構難しいイメージがあり(本来セキュリティ上そうであるべきだとも思い)ますが、こうして書いている筆者自身ここまで簡潔にできるとは思いませんでした。
ちなみに改めてストップウォッチではかってみたところ、20分程度でした!

ご覧いただきまして、ありがとうございました!

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?