Laravel Sanctum 如何自定义每个 token 的过期时间

之前论坛中有人提问过 问答: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 协议》,转载必须注明作者和本文链接
我从未见过一个早起、勤奋、谨慎,诚实的人抱怨命运。
本帖由系统于 3周前 自动加精
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 12

非常奈斯

1个月前 评论
chowjiawei

还有一种方式 很好用 并且可以做主题的 切换 那就是 config 修改配置文件 临时生效 只在本次失效

1个月前 评论
MArtian (楼主) 1个月前
chowjiawei (作者) 1个月前
MArtian (楼主) 1个月前

这包唯一蛋疼的地方就是每次请求都会有三条sql。。

1个月前 评论

什么时候不对数据库操作我在使用。不然我是不会使用这个包了,原本jwt的优势中就有减少数据库操作的优势。有私密数据可以多次加密也可以尽量保证数据安全。一般服务器secret不泄露的情况下都是安全的。

1个月前 评论

没想到这个人就是我 :see_no_evil:

3周前 评论
MArtian (楼主) 3周前

在 step 2 的时候,User 模型是不是还得重写 tokens()方法呢?不然在这张表 personal_access_tokens 中的过期时间就无法记录了。

    .
    .
    .
    /**
     * Get the access tokens that belong to model.
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
     */
    public function tokens(): \Illuminate\Database\Eloquent\Relations\MorphMany
    {
        return $this->morphMany(PersonalAccessToken::class, 'tokenable');
    }
1周前 评论
MArtian (楼主) 1周前
pretendtrue (作者) 1周前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!