Laravel Sanctum 如何自定義每個 token 的過期時間

MArtian發表於2022-04-09

之前論壇中有人提問過 問答:Sanctum 沒辦法手動設定過期時間吧?

如果要定義 Sanctum token 的過期時間,可以在 config/sanctum.php 中來統一定義。

'expiration'  =>  env("SANCTUM_TTL",  10080), 
'refresh_expiration'  =>  env("SANCTUM_REFRESH_TTL",  43200),

這樣的配置 token 的有效期是全域性生效的,例如:

token1 token2 token3
120m 120m 120m

但如果我們想要以下的方式呢?

token1 token2 token3
10m 60m 70m

要這麼做也很簡單,思路如下:

  1. 在 Sanctum 遷移檔案中新增 expired_at 欄位。
  2. 覆寫 HasApiToken 中的 createToken 方法。
  3. personal_access_tokens 表建立模型,並將 expired_at 寫入 fillable 屬性中。
  4. 在 Sanctum 的 authenticate 回撥方法中驗證 expired_at 來判斷 token 是否過期。

如果你的專案還沒有使用過 Sanctum

composer require laravel/sanctum

釋出 Sanctum 的配置檔案和遷移檔案:

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

Step1

來新增欄位到遷移檔案中,開啟 migrations/create_personal_access_token,將

$table->timestamp('expired_at')->nullable();

新增到 $table->timestamp('last_used_at')->nullable(); 之後。

$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('expired_at')->nullable(); // 新增
$table->timestamps();

執行遷移:

php artisan migrate

Step2

在 User 模型中使用 trait HasApiTokens,然後來複寫一下 其中的 createToken 方法。

// Models/User.php
public function createToken(string $name, array $abilities = ['*'], $expired_at = 3)
{
    $token = $this->tokens()->create([
        'name' => $name,
        'token' => hash('sha256', $plainTextToken = Str::random(40)),
        'abilities' => $abilities,
        'expired_at' => now()->addHours($expired_at) // 新增這行,先給它預設有效3小時。
]);

return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken);
}

Step3

為 Sanctum 建立模型

php artisan make:model PersonalAccessToken

新增 expired_atfillable 屬性:

use Laravel\Sanctum\PersonalAccessToken as Model; // 這裡要注意模型的繼承,NewAccessToken 控制器中只接受 `Laravel\Sanctum\PersonalAccessToken` 模型。
class PersonalAccessToken extends Model
{
    protected $casts = [
        'abilities' => 'json',
        'last_used_at' => 'datetime',
        'expired_at' => 'datetime'
    ];

    protected $fillable = [
        'name',
        'token',
        'abilities',
        'expired_at'
    ];
}

基於 Sanctum 的文件,如果你想要使用自定義的模型,需要在 providers/AuthServiceProvider.php 中註冊:

public function boot()
{

...

Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);

}

Step5

最後來讓我們修改 Sanctum 的認證回撥邏輯,預設情況下,它只會計算 token 的雜湊值來確保它是有效的。

這是 Sanctum 驗證 token 的程式碼邏輯:

protected function isValidAccessToken($accessToken): bool
{
    if (! $accessToken) {
        return false;
    }

    $isValid =
        (! $this->expiration || $accessToken->created_at->gt(now()->subMinutes($this->expiration)))
        && $this->hasValidProvider($accessToken->tokenable);

    if (is_callable(Sanctum::$accessTokenAuthenticationCallback)) {
        $isValid = (bool) (Sanctum::$accessTokenAuthenticationCallback)($accessToken, $isValid);
    }

    return $isValid;
}

注意這裡有一個判斷:

if(is_callable(Sanctum::$accessTokenAuthenticationCallback))

如果這個回撥方法存在,則 token 是否有效就取決於我們自定義的回撥。


再次開啟 providers/AuthServiceProvider.php 檔案,來註冊這個回撥

Sanctum::authenticateAccessTokensUsing(
    static function (PersonalAccessToken $accessToken, bool $is_valid) {
        // 自定義的驗證邏輯
    }
);

鑑於我們新增了自定義回撥,會影響現有的已經頒發 token 的使用者,所以這裡我們來做一個判斷,如果 expired_at 欄位不為 null,我們就檢查它是否過期,否則就不進行回撥處理。

return $accessToken->expired_at ? $is_valid && !$accessToken->expired_at->isPast() : $is_valid;

來測試一下

建立驗證控制器

php artisan make:controller Api\AuthorizationController.php
public function store(Request $request)
{
    if (!Auth::attempt($request->only(['email', 'password'])))     {
        abort(403);
    }

    $token = auth()->user()->createToken('api', ['*'], 5);
    return response()->json([
        'token' => $token->plainTextToken,
        'expired_at' => $token->accessToken->expired_at
    ]);
}

Laravel Sanctum 如何自定義每個 token 的過期時間

為了方便測試,我們手動更改一下表中的過期時間,將它改成過去的時間

Laravel Sanctum 如何自定義每個 token 的過期時間

api.php 路由中新增:

Route::group([
    'middleware' => [
        'auth:sanctum'
    ]
],function(){
    Route::get('test_token', function(){
        return 'token有效';
    }); 
});

Bearer Token 來訪問 your_project.test/api/test_token,下面我就不再演示了。

最後,token 的預設建立時間

上面的 createToken 方法中,我們預設先將 token 有效期設定為了 3 小時,下面我們來更改為 「如果沒有傳入有效期時,為它使用全域性配置」


開啟 config/sanctum.php 配置檔案,新增全域性配置:

'default_expiration'  =>  env("SANCTUM_DEFAULT_EXPIRATION",  10080),

改寫 createToken 方法為:

public function createToken(string $name, array $abilities = ['*'], $expired_at = null)
{
    $expired_at = $expired_at ?: config('sanctum.default_expiration');

    $token = $this->tokens()->create([
        'name' => $name,
        'token' => hash('sha256', $plainTextToken = Str::random(40)),
        'abilities' => $abilities,
        'expired_at' => now()->addHours($expired_at)
    ]);
    return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken);
}

這裡需要注意的一點是,如果我們將過期時間使用以上方法時,就不要再為配置檔案新增以下兩個配置:

refresh_expiration
expiration

否則 Sanctum 會在我們進行回撥驗證之前來決定 token 是否過期。

總結

這個解決方案不只適用於 User 模型,它是通用的,只要使用以上方法,可以為任意需要驗證的表來新增自定義有效期的 token

enjoy :tada:

本作品採用《CC 協議》,轉載必須註明作者和本文連結
我從未見過一個早起、勤奮、謹慎,誠實的人抱怨命運。

相關文章