【Laravel8】sanctumのtokenの有効期限をユーザーによって動的に変更する。認証API

はい、sanctumのtokenの有効期限を今回は変更したいと思います。といってもデフォルトで有効期限を設定できる場所はあります。

今回は有効期限を複数ユーザー、状態などで有効期限を動的に変更していきたいと思います!

こちらを参考にさせていただきました。

初めは↓の記事を参考にしていたのですが、Zennにて良さそうな記事があったのでZennの方の記事を最終的には参考にさせていただきました。

↓の記事も参考になると思うので目を通しておいてもよいかもです!

伝えたいこと: sanctumのtokenの有効期限をmiddleware使って変更する

今回tokenの有効期限をいろいろいじってみます。管理ユーザーと、会員ユーザーなど2つ以上ユーザーの種類があって、なおかつtokenの有効期限を別々に作成してみたい!

っていう方に向けて解決方法の一つを共有します。

ほかにもいろいろありますが今回はmiddlewareで制御する方法を共有しますね。

今回やること簡単に説明

やること

前提条件

  • Laravel8.xでの開発
  • すでにsanctumで認証する環境が整っている。

今回どんな事をするのか簡単に説明します。

personal_access_tokensテーブルにtokenの有効期限を持ったカラムを追加します。

ログインするたびに有効期限を更新するようにします。

そして、middlewareで対象のroutingが発生するときに、有効期限が切れていないのかどうかを確認していきます。

今回記事にないこと

  • sanctumの導入方法

sanctumで認証APIを作成する方法は以下の記事に載せているので確認してみて下さい。

【Laravel8】sanctumを使って認証apiを作ってみた。

personal_access_tokensを拡張

はい、まず今回はpersonal_access_tokensテーブルにtokenの有効期限を持ったカラムを追加しましょう。

今回はexpired_atカラムを追加します。ここに対象ユーザーのtokenの有効期限が追加されます。

<?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->timestamp('expired_at')->nullable()->comment('tokenの有効期限をユーザーごとに分ける');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('personal_access_tokens');
    }
}
Code language: HTML, XML (xml)

おそらくデフォルトでこちらのテーブルは準備されていると思います。

HasApiTokensトレイトを拡張

トレイトの拡張

では次にHasApiTokensトレイトを作成していきましょう。

通常でしたらapi/vendor/laravel/sanctum/src/HasApiTokens.php をユーザーモデルでトレイトすると思うのですが、ここで使用されているcreateToken() では先ほど先ほど作成したexpired_at カラムが更新できないんですね。

なので通常ユーザーモデルに適用されているHasAPiTokensトレイトのcreateTokenメソッドを置き換えるようにトレイトを拡張しましょう。

app/Http/Traits/HasApiTokens.php

<?php

namespace App\Http\Traits;

use Illuminate\Support\Str;
use Laravel\Sanctum\HasApiTokens as BaseHasApiTokens;
use Laravel\Sanctum\NewAccessToken;
use Datetime;

trait HasApiTokens
{
    use BaseHasApiTokens {
        BaseHasApiTokens::createToken as base_createToken;
    }

    /**
     * Create a new personal access token for the user.
     * 自作でcreateTokenのtraitsを作成して、ユーザーのモデルでuseする。
     * 通常のcreateTokenメソッドの場合追加したカラムexpired_atを更新できないので作成した。
     *
     * @param  string  $name
     * @param  int    $expiration_day //tokenの有効期限が切れる日がtodayから何日後か
     * @param  array  $abilities
     * @return \Laravel\Sanctum\NewAccessToken
     */

    //ユーザーがログインしたときにtokenを発行する
    public function createToken(string $name, int $expiration_day, array $abilities = ['*'])
    {
        $date = new DateTime();

        $token = $this->tokens()->create([
            'name' => $name,
            'token' => hash('sha256', $plainTextToken = Str::random(40)),
            'abilities' => $abilities,
            'expired_at' => $date->modify("+$expiration_day day")->format("Y-m-d H:i:s"),
        ]);

        //作成したtoken情報を返す
        return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken);
    }
}Code language: HTML, XML (xml)

Userモデルのトレイトを置き換える。

すでにsanctumの認証を利用している方は、HasApiTokensをuseしているかと思いますので、先ほど作成したトレイトに置き換えましょう。

もし、これから作成だ!っていう方は新しくHasApiTokensを追加してくださいね。

<?php

namespace App\Models;

...
...
...
// use Laravel\Sanctum\HasApiTokens; //旧
use App\Http\Traits\HasApiTokens; //新: 自作のHasApiTokenを利用
...
...
...

class User extends Authenticatable
{
    use HasApiTokens;

}Code language: HTML, XML (xml)

PersonalAccessTokenモデルクラスを作成

PersonalAccessTokenモデルを作成しましょう。

一応わかりやすいようにSanctumディレクトリを作成してそこに置きましょう。

php artisan make:model Sanctum/PersonalAccessToken

modelの中身は以下

<?php

namespace App\Models\Sanctum;

use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;

class PersonalAccessToken extends SanctumPersonalAccessToken
{
    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'expired_at' => 'datetime',
        'abilities' => 'json',
        'last_used_at' => 'datetime',
    ];

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'token',
        'expired_at',
        'abilities',
    ];

    /**
     * アクセストークンの有効性を独自チェックする
     *
     * @param mixed $accessToken
     * @param bool $isValid
     * @return bool
     */
    public static function isValidAccessToken($accessToken, bool $isValid)
    {
        if (!$accessToken->expired_at) {
            return $isValid;
        }
        return $accessToken->expired_at->gt(now());
    }

    /**
     * アクセストークンの有効期限が失効しているかチェックする
     *
     * @param mixed $accessToken
     * @param bool $isValid
     * @return bool
     */
    public static function isExpiredToken($accessToken)
    {
        if (!$accessToken->expired_at) {
            // nullの場合は失効扱い
            return true;
        }
        return $accessToken->expired_at->lte(now());
    }
}Code language: HTML, XML (xml)

拡張したクラスを適応させる

app/Providers/AppServiceProvider.phpにカスタムしたモデルの適応とアクセストークン認証のコールバックの適応を記述

<?php

namespace App\Providers;

use App\Models\Building;
use App\Models\Floor;
use App\Observers\BuildingObserver;
use App\Observers\FloorObserver;
use Illuminate\Support\ServiceProvider;

//追加
use Laravel\Sanctum\Sanctum;
use App\Models\Sanctum\PersonalAccessToken;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); //追加
    }
 
}
Code language: HTML, XML (xml)

有効期限チェックするミドルウェア作成

ミドルウェアの作成

ミドルウェアを作成して、毎度有効期限が切れていないかどうかをチェックします。

php artisan make:midleware VerifyExpiredApiTokenCode language: CSS (css)

app/Http/Middleware/verifyExpiredApiToken.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

//追加
use Illuminate\Auth\AuthenticationException;

class VerifyExpiredApiToken
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $user = $request->user();
        $token = !!$user ? $user->currentAccessToken() : null; //$userが真なら、今のtokenを、falseならnullを入れる。
        $token = $user->currentAccessToken();

        //tokenがnullもしくは有効期限が切れていたらエラーを返す
        if (!$token || $token->isExpiredToken($token)) { //PersonalAccessToken
            throw new AuthenticationException('API token expired.');
        }

        return $next($request);
    }
}
Code language: HTML, XML (xml)

ミドルウェアを適応させる

app/Http/Kernel.php

protected $routeMiddlewareのところに追加します。

    protected $routeMiddleware = [
        'verify.expired' => \App\Http\Middleware\VerifyExpiredApiToken::class, //ユーザーのtokenの有効期限チェックする
    ];Code language: PHP (php)

routing確認

今回はapiなので、routes/api.phpの中を書く

middlewareにsanctumとverify.expiredを指定する。

そうすることで認証が必要なrouting動作に+して、ログイン中のユーザーのtokenの有効期限も確認するようにしている。

Route::middleware('auth:sanctum', 'verify.expired')->group(function(){

});Code language: PHP (php)

ログイン/ログアウト

一応ログイン、ログアウトも書きのこしておく。

長いな…

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Models\User;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;

use Illuminate\Support\Facades\Hash;

use Illuminate\Validation\ValidationException;


class AuthenticatedSessionController extends Controller
{
    /**
     * Handle an incoming authentication request.
     *
     * @param  \App\Http\Requests\Auth\LoginRequest  $request
     * @return \Illuminate\Http\RedirectResponse
     */

    // ログインしたユーザに対してTokenの再生成とTokenにユーザが持っているabilities(認可)を付与する。
    public function store(LoginRequest $request)
    {
        //$request->session()->regenerate(); //tokenを使って認証をしたいのでコメントアウト

        //attemptメソッドの引数に渡すために変数にrequestを変数に入れる。
        $name = $request->name;
        $password = $request->password;

        //session認証処理を行い、成功したユーザーにはsanctumのapi tokenを発行する。
        if (Auth::attempt(['name' => $name, 'password' => $password])) {

            //token再生成に利用する為のログインユーザー情報を取得する。
            $user = Auth::user();
            $abilities = User::find($user->id)->abilities()->pluck('name')->toArray(); //tokenを取得したユーザのability情報を配列で取得(配列じゃないとabilityをcreateTokenメソッドの第二引数に指定できないから)

            //ログインユーザの持っている既存tokenを破棄して、新規にtokenを発行する。
            $user->tokens()->delete();

            //自動ログインをするのか、自動ログインをしないかでtokenの有効期限を$expiration_dayに定義する。
            switch($request->is_auto_login){
                case true:
                    $expiration_day=config('token_expiration_day.user.auto');
                    break;
                case false;
                    $expiration_day=config('token_expiration_day.user.non_auto');
                    break;
                default:
                    $expiration_day=config('token_expiration_day.user.non_auto');
            }

            $token = $user->createToken("login:user{$user->id}", $expiration_day, $abilities)->plainTextToken;

            //ログインするユーザーのlogin_ipとlast_login_atを更新する。
            $login_ip = $request->ip();
            $user->fill([
                $user->login_ip = $login_ip,
                $user->last_login_at = now()
            ])->save();

            Auth::logout(); //sanctumでsessionログインを使っていたらabilityがうまく使えなくなるからログアウト。tokenを利用

            //jsonで成功結果を返す
            return response()->json([
                'success' => true,
                'summary' => 'Login success!',
                'details' => [
                    'token' => $token,
                ]
            ], Response::HTTP_OK);
        }

        //エラーをjson形式で返す。
        return response()->json([
            'success' => false,
            'summary' => 'User Not Found.',
            'details' => [
                'name' => $name,
            ]
        ], Response::HTTP_INTERNAL_SERVER_ERROR);
    }

    /**
     * Destroy an authenticated session.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function destroy(Request $request)
    {
        //ログアウトするユーザーのlogin_ipとlast_login_atを更新する。
        $user = Auth::user();
        $login_ip = $request->ip();
        $user->fill([
            $user->login_ip = $login_ip,
            $user->last_login_at = now()
        ])->save();

        //response用に削除するtokenを取得
        $token = $request->header('Authorization');

        //tokenを削除
        $request->user()->tokens()->delete();

        //sessionはログインで使っていないので、コメントアウトにした。
        // $request->session()->invalidate();
        // $request->session()->regenerateToken();

        //jsonで結果を返す
        return response()->json([
            'success' => true,
            'summary' => 'Logout success!',
            'details' => [
                'name' => $request->user()->name,
                'token' => $token,
            ]
        ], 200);
        //return response()->setStatusCode()->json;
        //return response()->noContent();
    }
}
Code language: HTML, XML (xml)

tokenの有効期限をconfigに記述していたのでconfigも

config/token_expiration_day.php

<?php
/**
 * ユーザーごとにtokenの有効期限を設定する。
 *
 * auto=自動ログイン時の有効期限(day)
 * non_auto=自動ログイン無効時の有効期限(day)
 *
 */
return [
    'user'=>[
        'auto'=> 30,
        'non_auto' => 1
    ],
];Code language: HTML, XML (xml)

まとめ

以上です!最後のログイン、ログアウトのところちょっと雑になっていたけどこんな感じで作成してみました~

sanctum自体はシンプルな機能となっているのでmiddlewareで制御するのもありですね。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA