Laravel Auth Token 自定义认证


前几天用 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就可以进行身份验证了。

上一篇 下一篇

评论

登录后可发表评论