前几天用 Laravel 做了一套API接口,在认证Token时,Laravel默认会在user表的api_token字段验证,这样请求量大了数据库压力会很大。所以需要吧Token存在Redis进行验证,那么用Redis是如何验证Token呢?
【一】查找
在auth.php文件中我们可以看到 providers 的驱动仅支持 eloquent 或者 database,我们的重点就是让 providers 支持 redis。所以,我们需要找到providers 的驱动控制方法。经过一番查找最终在 Illuminate\Auth\CreatesUserProviders 中找到一个名为 createUserProvider 的方法。
...
public function createUserProvider($provider = null)
{
if (is_null($config = $this->getProviderConfiguration($provider))) {
return;
}
if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) {
return call_user_func(
$this->customProviderCreators[$driver], $this->app, $config
);
}
switch ($driver) {
case 'database':
return $this->createDatabaseProvider($config);
case 'eloquent':
return $this->createEloquentProvider($config);
default:
throw new InvalidArgumentException(
"Authentication user provider [{$driver}] is not defined."
);
}
}
...
首先获取 auth.providers 里的配置,如果存在 $this->customProviderCreators 自定义 Provider 创建方法,调用该方法创建 Provider;否则就根据传入的 $provider 参数创建内建的 Provider。
这里的 $this->customProviderCreators 就是我们创建自定义 Provider 的关键了。
查看代码,发现在 Illuminate\Auth\AuthManager 里的 provider() 方法对这个数组进行了赋值:
...
public function provider($name, Closure $callback)
{
$this->customProviderCreators[$name] = $callback;
return $this;
}
...
传入两个参数: $name , Provider 的标识; $callback , Provider 创建闭包。
就是通过调用这个方法,传入自定义 Provider 创建方法,就会可以把自定义的 Provider 放入使用的 guard 中,以达到我们的目的。
【二】定位
首先是使用 Redis 的 Provider。
供 Auth 使用的 Provider 必须实现 Illuminate\Contracts\Auth\UserProvider 接口:
这个接口给出了几个方法:
· retrieveById() : 通过 id 获取用户
· retrieveByToken() : 通过 token 获取用户。注意,这里的 token 不是我们要用的 api_token ,是 remember_token
· updateRememberToken() : 更新 remember_token
· retrieveByCredentials() : 通过凭证获取用户,比如用户名、密码,或者我们这里要用到的 api_token
· validateCredentials() : 验证凭证,比如验证用户密码
我们的需求就是 api 传入 api_token ,再通过存在 Redis 里的 api_token 来获取用户,并交给 guard 使用。
上面我们看到了,在 Illuminate\Auth\TokenGuard 里:
...
public function user()
{
// If we've already retrieved the user for the current request we can just
// return it back immediately. We do not want to fetch the user data on
// every call to this method because that would be tremendously slow.
if (! is_null($this->user)) {
return $this->user;
}
$user = null;
$token = $this->getTokenForRequest();
if (! empty($token)) {
$user = $this->provider->retrieveByCredentials([
$this->storageKey => $this->hash ? hash('sha256', $token) : $token,
]);
}
return $this->user = $user;
}
...
是调用 $this->provider->retrieveByCredentials() 根据凭证获取用户的,我们用 api_token 换用户的操作在这个方法里实现。
根据需求,最方便的方法是复用 Illuminate\Auth\EloquentUserProvider 类,并重载 retrieveByCredentials() 方法,就可以实现我们想要的功能了。
【三】新建
Exceptions目录下新建文件RedisUserProvider.php
<?php
namespace App\Exceptions;
use Illuminate\Support\Facades\Redis;
use Illuminate\Auth\EloquentUserProvider;
class RedisUserProvider extends EloquentUserProvider
{
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
if (!isset($credentials['api_token'])) return ;
$userId = Redis::GET('user_tokens:' . $credentials['api_token']);
if ($userId) {
$user = Redis::GET('user_infos:' . $userId); //从redis中读取
return json_decode($user);
}
// if ($userId) return $this->retrieveById($userId); //从数据库读取
}
}
好了,剩下要做的就是在 TokenGuard 里使用这个 Provider 了。
前面我们提到在 Illuminate\Auth\AuthManager::provider() 里设置 customProviderCreators 可以达成这个目的。
找到位置就好办,我们在 App\Providers\AppServiceProvider 注册这个方法:
...
use App\Exceptions\RedisUserProvider;
...
public function register()
{
$this->app->make('auth')->provider('redis', function ($app, $config) {
return new RedisUserProvider($app['hash'], $config['model']);
});
}
...
现在,我们在控制器中就可以接收到redis返回的用户信息了
...
use Illuminate\Http\Request;
...
public function __construct(Request $request)
{
$this->request = $request;
}
public function info()
{
$user = $this->request->user();
dump($user);
}
【四】结合
通过VerifyCsrfToken来结合Token验证
...
use Illuminate\Support\Facades\Auth;
...
protected $except = [
// '*',
];
public function handle($request, Closure $next)
{
if(!$this->inExceptArray($request)){
if (Auth::guest()) {
$result = [
'data' => (object)[],
'code' => 300,
'msg' => 'Token验证失败,请重新登录',
];
return response()->json($result);
}
}
return $next($request);
}
一切就绪,现在只需要在接口中传入正确的Authorization就可以进行身份验证了。
登录后可发表评论