- 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\AuthManager
的 guard()
方法,看看这个 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_tokenupdateRememberToken()
: 更新 remember_tokenretrieveByCredentials()
: 通过凭证获取用户,比如用户名、密码,或者我们这里要用到的 api_tokenvalidateCredentials()
: 验证凭证,比如验证用户密码
我们的需求就是 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
可以看到返回用户信息。