最近商城项目中遇到一个Token
问题,前后台登录需要区分普通用户
与 内部员工
,但是 Laravel
内置的看守器,并不能满足我的需求。因为它总是返回一个当前Provider(提供者)
,换句话说,如果我的提供者模型是关联的 Token
模型,那么它在解析前台传来的 token
后,只会返回Token
这个模型:
按照我的需求,目前 User
和 Employee
模型需要共用一个守卫,并且守卫表要进行区分普通用户与内部员工,并且守卫的 user()
方法解析token
后必须得返回User
或 Employee
其中一个模型。
源码解析
内置的TokenGuard.php
位于 vendor/laravel/framework/src/Illuminate/Auth/TokenGuard.php
,
打开它,可以发现它继承了 Guard
接口 :
namespace Illuminate\Auth;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
class TokenGuard implements Guard
{
use GuardHelpers;
//..........
}
这个接口方法也大多是检测当前用户是否登录、获取当前用户等。内部所引入的 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模型与表
模型用来关联 User
和 Employee
,方便自定义守卫后重写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',
// ],
],
我们让 user
和 employee
守卫 的驱动都注册为 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();
}