Larave Auth Token 认证使用自定义 Redis UserProvider

匿名 (未验证) 提交于 2019-12-03 00:44:02
  • PHP: 7.2
  • Larave 5.6

用 Laravel 做一套接口,需要用到 token 认证。
接口调用频繁,有心跳链接,如果 token 在数据库中,数据库压力会很大,所以用 Redis 保存用户 Token 。

但是 Larave 自带的获取用户的 Provider 只支持 eloquent 或者 database ,并不支持使用 Redis ,所以需要自己写一个支持 Redis 的 Provider。
怎么写这个 Provider ,并且能无缝的融入到 Laravel 自己的 Auth 认证呢?只能从源码开始分析了。

Larave 已经搭好 api 的框架,在 routes/api.php 里已经写了一个路由:

...  Route::middleware('auth:api')->get('/user', function (Request $request) {     return $request->user(); });  ...

这里使用了 auth 中间件,并传入 api 参数。

auth 中间件在 app/Http/Kernel.php 中可以找到定义:

    ...      protected $routeMiddleware = [         'auth' => \Illuminate\Auth\Middleware\Authenticate::class,         'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,         'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,         'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,         'can' => \Illuminate\Auth\Middleware\Authorize::class,         'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,         'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,         'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,     ];      ...

可以看到 auth 中间件对应的是 \Illuminate\Auth\Middleware\Authenticate 这个类,这里需要关注的代码:

    ...      public function __construct(Auth $auth)     {         $this->auth = $auth;     }      ...      public function handle($request, Closure $next, ...$guards)     {         $this->authenticate($guards);          return $next($request);     }      ...      protected function authenticate(array $guards)     {         if (empty($guards)) {             return $this->auth->authenticate();         }          foreach ($guards as $guard) {             if ($this->auth->guard($guard)->check()) {                 return $this->auth->shouldUse($guard);             }         }          throw new AuthenticationException('Unauthenticated.', $guards);     }      ...

Authenticate 在初始化的时候,由系统自动注入 Illuminate\Contracts\Auth\Factory 的对象,这里实例化之后是 Illuminate\Auth\AuthManager 的对象,再赋值给 $this->auth

handel 方法调用自己的 authenticate() 方法,并传入中间件参数 $guards,这里传入的参数就是 api

如果没有传入 $guards 参数,就调用 $this->auth->authenticate() 进行验证。

这个 authenticate() 方法在 Illuminate\Auth\AuthManager 里找不到,但是 AuthManager 定义了一个魔术方法 __call()

    ...      public function __call($method, $parameters)     {         return $this->guard()->{$method}(...$parameters);     }      ...

如果当前类没有改方法,就调用 $this->guard() 返回的对象的方法,这里 $this->guard() 又是啥:

    ...      public function guard($name = null)     {         $name = $name ?: $this->getDefaultDriver();          return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);     }      ...

这里暂时先不展开,只需要知道方法返回在 auth 配置里配置的 guard

    ...      'guards' => [         'web' => [             'driver' => 'session',             'provider' => 'users',         ],          'api' => [             'driver' => 'token',             'provider' => 'token-user',         ],     ],      ...

这里的 guard 是 api,使用的 driver 是 token,对应 Illuminate\Auth\TokenGuard

TokenGuard 类里使用了 GuardHelpers 这个 trait ,authenticate() 方法就定义在这个 trait 里:

    ...      public function authenticate()     {         if (! is_null($user = $this->user())) {             return $user;         }          throw new AuthenticationException;     }      ...

这里判断 $this->user() 如果为空,就抛出 \Illuminate\Auth\AuthenticationException 异常,登陆失败;

再来看看 $this->user() 是什么鬼,定义在 Illuminate\Auth\TokenGuard 里:

    ...      public function user()     {         if (! is_null($this->user)) {             return $this->user;         }          $user = null;          $token = $this->getTokenForRequest();          if (! empty($token)) {             $user = $this->provider->retrieveByCredentials(                 [$this->storageKey => $token]             );         }          return $this->user = $user;     }      ...

user() 方法里,首先判断 $this->user 是否存在,如果存在,直接返回;

如果 $this->user 不存在,调用 $this->getTokenForRequest() 方法获取 token:

    ...      public function getTokenForRequest()     {         $token = $this->request->query($this->inputKey);          if (empty($token)) {             $token = $this->request->input($this->inputKey);         }          if (empty($token)) {             $token = $this->request->bearerToken();         }          if (empty($token)) {             $token = $this->request->getPassword();         }          return $token;     }      ...

这里的 $this->inputKey 在构造函数里给的 api_token

    ...      public function __construct(UserProvider $provider, Request $request, $inputKey = 'api_token', $storageKey = 'api_token')     {         $this->request = $request;         $this->provider = $provider;         $this->inputKey = $inputKey;         $this->storageKey = $storageKey;     }      ...

再回到 getTokenForRequest() 里:

首先在传入的查询字符串里获取 api_token 的值;

如果不存在,在 $request->input() 里找;

还是不存在,通过 $request->bearerToken() 在请求头里获取,相应的代码在 Illuminate\Http\Concerns\InteractsWithInput 里:

    ...      public function bearerToken()     {         $header = $this->header('Authorization', '');          if (Str::startsWith($header, 'Bearer ')) {             return Str::substr($header, 7);         }     }      ...

请求头里的字段是 Authorization ,需要注意的是,这里的 token 要以字符串 Bearer 开头,Laravel 会自动将前面的 Bearer 去掉并返回。

还是回到 getTokenForRequest() 里:

如果以上途径都没有获取到 token 那么就把请求传入的密码作为 token 返回。

回到 Illuminate\Auth\TokenGuard::user() 方法,使用获取的 token 调用 $this->provider->retrieveByCredentials() 方法获取用户。

我们再来看一下 $this->provider 是从哪里来的,在 Illuminate\Auth\TokenGuard 的构造函数里:

    ...      public function __construct(UserProvider $provider, Request $request, $inputKey = 'api_token', $storageKey = 'api_token')     {         $this->request = $request;         $this->provider = $provider;         $this->inputKey = $inputKey;         $this->storageKey = $storageKey;     }      ...

第一个参数就是 $provider,我们再回到 Illuminate\Auth\AuthManagerguard() 方法,看看这个 guard 是怎么创建的:

    ...      public function guard($name = null)     {         $name = $name ?: $this->getDefaultDriver();          return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);     }      ... 

如果没有传入 $guard 参数,就调用 $this->getDefaultDriver() 获取默认的 guard 驱动

    ...      public function getDefaultDriver()     {         return $this->app['config']['auth.defaults.guard'];     }      ...

返回配置文件 config/auth.php 中的值 web

    ...      'defaults' => [         'guard' => 'web',         'passwords' => 'users',     ],      ...

然后再判断 $this->guards[] 里是否存在这个 guard ,如果不存在,通过 $this->resolve($name) 生成 guard 并返回。

再看看这里的 resolve() 方法:

    ...      protected function resolve($name)     {         $config = $this->getConfig($name);          if (is_null($config)) {             throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");         }          if (isset($this->customCreators[$config['driver']])) {             return $this->callCustomCreator($name, $config);         }          $driverMethod = 'create'.ucfirst($config['driver']).'Driver';          if (method_exists($this, $driverMethod)) {             return $this->{$driverMethod}($name, $config);         }          throw new InvalidArgumentException("Auth driver [{$config['driver']}] for guard [{$name}] is not defined.");     }      ...

首先获取 guard 的配置,如果配置不存在,直接抛出异常;

再看是否存在自定义的 guard 创建方法 $this->customCreators(这里的 $this->customCreator 通过 extend() 方法配置),如果存在,就调用自定义创建 guard 的方法;

没有自定义 guard 方法,就调用类里写好的 createXXXXDriver() 方法创建 guard ,这里就是 createTokenDriver()

    ...      public function createTokenDriver($name, $config)     {         $guard = new TokenGuard(             $this->createUserProvider($config['provider'] ?? null),             $this->app['request']         );          $this->app->refresh('request', $guard, 'setRequest');          return $guard;     }      ...

总算找到创建 guard 的地方了,这里又调用了 $this->createUserProvider() 方法创建 Provider ,并传入 guard 的构造函数,创建 guard ,这个 createUserProvider() 方法是写在 Illuminate\Auth\CreatesUserProviders 这个 trait 里的:

    ...      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 接口:

 interface UserProvider {     public function retrieveById($identifier);      public function retrieveByToken($identifier, $token);      public function updateRememberToken(Authenticatable $user, $token);      public function retrieveByCredentials(array $credentials);      public function validateCredentials(Authenticatable $user, array $credentials); } 

这个接口给出了几个方法

  • 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 (! is_null($this->user)) {             return $this->user;         }          $user = null;          $token = $this->getTokenForRequest();          if (! empty($token)) {             $user = $this->provider->retrieveByCredentials(                 [$this->storageKey => $token]             );         }          return $this->user = $user;     }      ...

是调用 $this->provider->retrieveByCredentials() 根据凭证获取用户的,我们用 api_token 换用户的操作在这个方法里实现。

根据需求,最方便的方法是复用 Illuminate\Auth\EloquentUserProvider 类,并重载 retrieveByCredentials() 方法,就可以实现我们的需求了

 namespace App\Extensions;  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($credentials['api_token']);          return $this->retrieveById($userId);     } }  

这里简单起见,我在 Redis 里只存储了用户 id ,再调用 retrieveById() 获取用户,真正的应用中,可以根据需要,在 Redis 里存入需要的数据,直接取出,提高效率。

Provider 写好了,剩下要做的就是在 TokenGuard 里使用这个 Provider 了。

前面我们提到在 Illuminate\Auth\AuthManager::provider() 里设置 customProviderCreators 可以达成这个目的。

找到位置就好办,我们在 App\Providers\AppServiceProvider 注册这个方法:

    ...      use App\Extensions\RedisUserProvider;      ...      public function register()     {         $this->app->make('auth')->provider('redis', function ($app, $config) {             if (is_null($config = $app['config']['auth.providers.token-user'])) {                 return;             }              return new RedisUserProvider($app['hash'], $config['model']);         });     }      ...

至此,完成。

我们在 Redis 存入一个以 token 为 key 的用户 id,在浏览器里输入 http://localhost/api/user?api_token=XXXX 可以看到返回用户信息。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!