Larave Auth Token 认证使用自定义 Redis UserProvider
Larave Auth Token 认证使用自定义 Redis UserProvider
软件环境
- 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) {
return new RedisUserProvider($app['hash'], $config['model']);
});
}
...
至此,完成。
测试
我们在 Redis 存入一个以 token 为 key 的用户 id,在浏览器里输入 http://localhost/api/user?api_token=XXXX
可以看到返回用户信息。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: