はい、sanctumのtokenの有効期限を今回は変更したいと思います。といってもデフォルトで有効期限を設定できる場所はあります。
今回は有効期限を複数ユーザー、状態などで有効期限を動的に変更していきたいと思います!
こちらを参考にさせていただきました。
初めは↓の記事を参考にしていたのですが、Zennにて良さそうな記事があったのでZennの方の記事を最終的には参考にさせていただきました。
↓の記事も参考になると思うので目を通しておいてもよいかもです!
Table of Contents
伝えたいこと: 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 VerifyExpiredApiToken
Code 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で制御するのもありですね。