最近商城项目中遇到一个Token问题,前后台登录需要区分普通用户内部员工,但是 Laravel 内置的看守器,并不能满足我的需求。因为它总是返回一个当前Provider(提供者),换句话说,如果我的提供者模型是关联的 Token 模型,那么它在解析前台传来的 token 后,只会返回Token这个模型:
自定义Guard - 图1

按照我的需求,目前 UserEmployee 模型需要共用一个守卫,并且守卫表要进行区分普通用户与内部员工,并且守卫的 user()方法解析token后必须得返回UserEmployee其中一个模型。

源码解析

内置的TokenGuard.php 位于 vendor/laravel/framework/src/Illuminate/Auth/TokenGuard.php,
打开它,可以发现它继承了 Guard 接口 :

  1. namespace Illuminate\Auth;
  2. use Illuminate\Contracts\Auth\Guard;
  3. use Illuminate\Contracts\Auth\UserProvider;
  4. use Illuminate\Http\Request;
  5. class TokenGuard implements Guard
  6. {
  7. use GuardHelpers;
  8. //..........
  9. }

这个接口方法也大多是检测当前用户是否登录、获取当前用户等。内部所引入的 GuardHelpers实际上是实现了
Guard 接口内的大多数方法,作者的目的也许是为了方便用户自定义guard 后引入这个Trait,而不需要去重写这些方法。
那么,内置的看守器是如何被实例化的呢?打开 vendor/laravel/framework/src/Illuminate/Auth/AuthManager.php,可以看到里面有一段方法:

    /**
     * Create a token based authentication guard.
     *
     * @param  string  $name
     * @param  array  $config
     * @return \Illuminate\Auth\TokenGuard
     */
    public function createTokenDriver($name, $config)
    {
        // The token guard implements a basic API token based guard implementation
        // that takes an API token field from the request and matches it to the
        // user in the database or another persistence layer where users are.
        $guard = new TokenGuard(
            $this->createUserProvider($config['provider'] ?? null),
            $this->app['request'],
            $config['input_key'] ?? 'api_token',
            $config['storage_key'] ?? 'api_token',
            $config['hash'] ?? false
        );

        $this->app->refresh('request', $guard, 'setRequest');

        return $guard;
    }


      /**
     * Register a new callback based request guard.
     *
     * @param  string  $driver
     * @param  callable  $callback
     * @return $this
     */
    public function viaRequest($driver, callable $callback)
    {
        return $this->extend($driver, function () use ($callback) {
            $guard = new RequestGuard($callback, $this->app['request'], $this->createUserProvider());

            $this->app->refresh('request', $guard, 'setRequest');

            return $guard;
        });
    }


   /**
     * Register a custom driver creator Closure.
     *
     * @param  string  $driver
     * @param  \Closure  $callback
     * @return $this
     */
    public function extend($driver, Closure $callback)
    {
        $this->customCreators[$driver] = $callback;

        return $this;
    }

源码大概流程是,Laravel 启动后,会获取 config.php 的守卫配置,运行 viaRuest()方法,然后调用 extdnd() 方法,为用户定义的驱动注册一个相应的 Token 守卫:['token' => fn () => TokenGuard::clas],当访问应用时,使用 auth() 辅助函数去服务容器里去寻找这个 key值,获取之后会运行 key所对应的闭包。懂得源理后。着手自定义一个属于自己的守卫。

Token模型与表

模型用来关联 UserEmployee ,方便自定义守卫后重写user()方法,返回相应的关联模型。Laravel源码其实也为我们准备了这个模型。但是它的字段并不是我们想要的。我们也不可能每次更新框架后再去更新源码中的模型。所以,我们再创建一个来覆盖框架自带的

php artisan make:model PersonalAccessToken

数据表是Laravel8开始就自带的迁移文件:2019_12_14_000001_create_personal_access_tokens_table.php,这里我们将数据表的字段进行修改:

public function up()
    {
        Schema::create('personal_access_tokens', function (Blueprint $table) {
            $table->id();
            $table->enum('guard', ['api', 'passport'])->default('api')->comment('token 类型');
            $table->string('name')->comment('token 名称');
            $table->string('api_token', 64)->unique();
            $table->text('abilities')->nullable()->comment('能力');
            $table->timestamp('last_used_at')->nullable()->comment('最后使用时间');
            $table->timestamp('expires_in')->nullable()->comment('过期时间');
            $table->unsignedInteger('user_id')->nullable()->comment('token 所属用户')->unique()->index();
            $table->unsignedInteger('employee_id')->nullable()->comment('token 所属员工')->unique()->index();
            $table->timestamps();
        });
    }

其中 guard字段就是绑定的自定义守卫驱动,完成编辑后。我们开始配置自定义守卫

配置自定义守卫

编辑 auth.php

// 默认守卫
'defaults' => [
  'guard'     => 'api',
  'passwords' => 'users',
],

// 守卫组
'guards' => [
  'web'      => [
    'driver'   => 'session',
    'provider' => 'users',
  ],
  'user'      => [
    'driver'     => 'passport',
    'provider'   => 'users',
    'hash'       => true,
  ],
  'employee' => [
    'driver'     => 'passport',
    'provider'   => 'users',
    'hash'       => true,
  ],
],

// 提供者
'providers' => [
  'users' => [
    'driver' => 'eloquent',
    'model'  => App\Models\PersonalAccessToken::class,
  ],
  // 'users' => [
  //     'driver' => 'database',
  //     'table' => 'users',
  // ],
    ],

我们让 useremployee 守卫 的驱动都注册为 passport,而且将它们的提供者都指向同一个提供者,这样就能实现共用。
配置完之后,创建自定义守卫

<?php

declare(strict_types=1);

namespace App\ThirdParty\Auth\Guard;

use App\Models\PersonalAccessToken;
use App\ThirdParty\Auth\Traits\GuardHelpers;
use Illuminate\Auth\CreatesUserProviders;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard as GuardInterface;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use App\Enums\Guard as EnumGuard;
use Illuminate\Support\Facades\Auth;

abstract class Guard implements GuardInterface
{
    use CreatesUserProviders,GuardHelpers;

    protected readonly Request      $req;
    protected readonly string       $inputKey;
    protected readonly string       $storageKey;
    protected readonly bool         $hash;
    protected readonly UserProvider $provider;
    protected readonly Application  $app;

    protected PersonalAccessToken|Authenticatable|null $model = null;

    final public function __construct(
        string      $provider,
        Request     $request,
        Application $app,
        string      $inputKey = 'api_token',
        string      $storageKey = 'api_token',
        bool        $hash = false,
    )
    {
        $this->app = $app;
        $this->provider = $this->createUserProvider($provider);
        $this->req = $request;
        $this->hash = $hash;
        $this->inputKey = $inputKey;
        $this->storageKey = $storageKey;
    }

    /**
     * Get relation model
     * @return Model|null
     */
    final public function user(): ?Model
    {
        if (!is_null($this->model)) {
            return $this->resolveModel($this->getGuard());
        }

        $model = null;

        if (!empty($accessToken = $this->getTokenForRequest())) {
            $model = $this->provider->retrieveByCredentials([
                $this->storageKey => $this->hash ? hash('sha256', $accessToken) : $accessToken
            ]);
        }

        $this->model = $model;

        return $this->resolveModel($this->getGuard());
    }

    private function getTokenForRequest(): string
    {
        $accessToken = $this->req->query($this->inputKey);

        if (empty($accessToken)) {
            $accessToken = $this->req->input($this->inputKey);
        }

        if (empty($accessToken)) {
            $accessToken = $this->req->bearerToken();
        }

        if (empty($accessToken)) {
            $accessToken = $this->req->getPassword();
        }

        return $accessToken;
    }

    /**
     * Get current token model guard type
     * @return EnumGuard
     */
    final protected function getGuard(): EnumGuard
    {
        return $this->model->guard;
    }

    /**
     * By resolve guard get relation model
     * @param EnumGuard $guard
     * @return Model|null
     */
    final protected function resolveModel(EnumGuard $guard): ?Model
    {
        return match ($guard) {
            EnumGuard::USER     => $this->model->user ?? null,
            EnumGuard::EMPLOYEE => $this->model->employee ?? null,
        };
    }

    /**
     * Register new token guard
     * @return void
     */
    final static public function extend(): void
    {
        Auth::extend('passport', function ($app, $name, $config) {
            $guard = new PassportGuard(
                $config['provider'],
                $app['request'],
                $app,
                $config['input_key'] ?? 'api_token',
                $config['storage_key'] ?? 'api_token',
                hash: $config['hash']
            );

            $app->refresh('request', $guard, 'setRequest');

            return $guard;
        });
    }
}
<?php

declare(strict_types=1);

namespace App\ThirdParty\Auth\Guard;

class PassportGuard extends Guard
{
    /**
     * Verify token is expires
     * @return bool
     */
    public function expires(): bool
    {
        return $this->model->expires_in > time();
    }

    /**
     * Logout current relation model
     * @return void
     */
    public function logout(): void
    {
        $this->model->api_token = null;
        $this->model->save();
    }
}
<?php

namespace App\ThirdParty\Auth\Traits;

use Illuminate\Contracts\Auth\Authenticatable;

trait GuardHelpers
{
    public function check(): bool
    {
        return !is_null($this->user());
    }

    public function guest(): bool
    {
        return !$this->check();
    }

    public function id(): int
    {
        return $this->resolveModel($this->getGuard())->id;
    }

    public function hasUser(): bool
    {
        return !is_null($this->user());
    }

    public function setUser(Authenticatable $user): void
    {
        $this->model = $user;
    }

    final public function validate(array $credentials = []): bool
    {
        if (empty($credentials[$this->inputKey])) {
            return false;
        }

        $credentials = [$this->storageKey => $credentials[$this->inputKey]];

        if ($this->provider->retrieveByCredentials($credentials)) {
            return true;
        }

        return false;
    }

}

编辑AppServiceProvider.php

  public function boot()
    {
        // Register custom auth
        Guard::extend();
    }

自定义Guard - 图2
自定义Guard - 图3