Laravel Auth实现多表多字段用户认证

Laravel Auth实现多表多字段用户认证

laravel提供了开箱即用的用户登录功能,6.0之前之前php artisan make:auth,6.0之后需要安装laravel/ui,然后执行

php artisan ui vue --auth

npm install && npm run dev

php artisan migrate 至此我们就拥有关于认证的视图、路由、控制器了

执行php artsian route:list 查看配套的路由 我们先看登录功能,由此引出auth认证,最终实现一个基于web的多表多字段的认证

# 直奔主题Auth::attempt,未讲解代码可自行查看
# App\Http\Controllers\Auth\LoginController@login
# 登录的主要代码位于Illuminate\Foundation\Auth\AuthenticatesUsers

public function login(Request $request)
{
    $this->validateLogin($request);
	// 这部分代码是限流trait带来的 限流器以后会单独讲解
    if (method_exists($this, 'hasTooManyLoginAttempts') &&
        $this->hasTooManyLoginAttempts($request)) {
        $this->fireLockoutEvent($request);

        return $this->sendLockoutResponse($request);
    }
	// 今天的重点由此引出
    if ($this->attemptLogin($request)) {
        return $this->sendLoginResponse($request);
    }

    $this->incrementLoginAttempts($request);

    return $this->sendFailedLoginResponse($request);
}

// 调用了guard的attempt方法
/**
 * Attempt to log the user into the application.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return bool
 */
protected function attemptLogin(Request $request)
{
    return $this->guard()->attempt(
        $this->credentials($request), $request->filled('remember')
    );
}

我有点感觉了 你们呢?一起来看Auth门面提供的attempt方法 Auth门面的对应的实例为AuthManager,为什么叫manager呢,就是因为确实是一个管理器,laravel通过guard provider等配置提供了多种的认证方式。前面有关于门面讲解的文章。

# 我们尝试调用Auth::attempt()方法 实际触发的是AuthManager::__call方法
/**
 * Dynamically call the default driver instance.
 *
 * @param  string  $method
 * @param  array  $parameters
 * @return mixed
 */
public function __call($method, $parameters)
{
    return $this->guard()->{$method}(...$parameters);
}

# 接着看guard方法
public function guard($name = null)
{   
    // getDefaultDriver非常简单返回的是web 如果你们修改相关auth.php配置的话
    $name = $name ?: $this->getDefaultDriver();
	// 这是一个类似单例的写法 将解析出来的guard实例保存在数组中,下次直接使用时直接返回
    return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}

# 接着看resolve方法
protected function resolve($name)
{	
    // getConfig获取给定的guard的配置
    // 'web' => [
    //     'driver' => 'session',
    //     'provider' => 'users',
    // ],
    $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';
    // dd($driverMethod); // createSessionDriver   我们接着看createSessionDriver方法	

    if (method_exists($this, $driverMethod)) {
        // 默认的情况下是 $name = web
        return $this->{$driverMethod}($name, $config);
    }

    throw new InvalidArgumentException(
        "Auth driver [{$config['driver']}] for guard [{$name}] is not defined."
    );
}

// 创建一个session为驱动的guard实例
public function createSessionDriver($name, $config)
{   
    // $config == 'users'
    $provider = $this->createUserProvider($config['provider'] ?? null);
	// 可以看到实例化guard的时候需要provider实例
    // 至此你是否更加了解auth.php的配置格式了呢?
    $guard = new SessionGuard($name, $provider, $this->app['session.store']);
	...

    return $guard;
}

// 接着看createUserProvider方法  位于Illuminate\Auth\CreatesUserProviders
public function createUserProvider($provider = null)
{
    if (is_null($config = $this->getProviderConfiguration($provider))) {
        return;
    }

    // driver = 'eloquent'
    // 可以看到laravel允许我们提供自己的provider
    if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) {
        return call_user_func(
            $this->customProviderCreators[$driver], $this->app, $config
        );
    }
	
    // 走到eloquent分支
    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."
            );
    }
}

// 接着查看createEloquentProvider方法
protected function createEloquentProvider($config)
{	
    return new EloquentUserProvider($this->app['hash'], $config['model']);
}
# 好的,我们可以返回到AuthManager@guard方法了 为我们返回了一个SessionGuard实例
# 所以我们Auth::attempt() 实际调用的是SessionGuard的attempt方法

# Illuminate\Auth\SessionGuard
public function attempt(array $credentials = [], $remember = false)
{
    $this->fireAttemptEvent($credentials, $remember);
	// $this->provider就是实例化传递进来的EloquentUserProvider
    $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

    if ($this->hasValidCredentials($user, $credentials)) {
        $this->login($user, $remember);

        return true;
    }
    
    $this->fireFailedEvent($user, $credentials);

    return false;
}

# 前面说过既然auth.php允许我们配置自己的provider那么当然可以通过我们自己的UserProvider来重写这段找用户的代码了
# 这就是今天要说的多表多字段认证 我们重写此方法可以通过上传的各种字段进行用户实例的查找
public function retrieveByCredentials(array $credentials)
{
    if (empty($credentials) ||
        (count($credentials) === 1 &&
         Str::contains($this->firstCredentialKey($credentials), 'password'))) {
        return;
    }

    $query = $this->newModelQuery();

    foreach ($credentials as $key => $value) {
        if (Str::contains($key, 'password')) {
            continue;
        }
		// laravel试图将认证字段拼接成查询条件
        if (is_array($value) || $value instanceof Arrayable) {
            $query->whereIn($key, $value);
        } else {
            $query->where($key, $value);
        }
    }
	// 返回第一个符合的model实例 此model就是在auth.php中配置的model
    return $query->first();
}

# 接着看hasValidCredentials方法 判断找到第一个能够通过验证
# 这soc还是非常值得学习的啊 
# 也就是说我们通过重写UserProvider的validateCredentials方法就能够最终决定用户是否能够登录应用
protected function hasValidCredentials($user, $credentials)
{	
    return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
}

# 接着看UserProvider的validateCredentials方法
# 通过实例化EloquentUserProvider传递进去的hasher进行密码校验
/**
 * Validate a user against the given credentials.
 *
 * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
 * @param  array  $credentials
 * @return bool
 */
public function validateCredentials(UserContract $user, array $credentials)
{
    $plain = $credentials['password'];

    // getAuthPassword在user模型继承的接口中 返回的是$this->password
    return $this->hasher->check($plain, $user->getAuthPassword());
}

# 如果校验成功就返回true 失败返回false 我们通过attempt方法的返回值就可以判断用户是合法,从而将用户log到我们的app中

理解了上面的大体流程,来实现下多表多字段登录吧

# 我们知道了通过重写UserProvider的方法可以实现对登录完全的掌控,但是如何让自定义的UserProvider生效呢?
# 还记得AuthManager的customProviderCreators属性吗,其实是引入的trait带来的
# 我们可以通过provider方法注册我们自己的UserProvider
# 在哪注册合适呢 当然是AuthServiceProvider的boot方法了

1 创建一个UserProvider  
# 重写EloquentUserProvider的retrieveByCredentials方法
# 你当然还可以继续重写validateCredentials方法 这里只是娱乐举个例子
<?php

namespace App\Services;

use Illuminate\Auth\EloquentUserProvider as BaseProvider;

class EloquentUserProvider extends BaseProvider
{
    public function retrieveByCredentials(array $credentials)
    {
        if (
            empty($credentials) || (count($credentials) === 1 &&
                array_key_exists('password', $credentials))
        ) {
            return;
        }

        $query = $this->createModel()->newQuery();
		// 没什么实际意义 我瞎写的代码 
        // 各位可在此编写符合自己业务的用户查找代码
        $rawWhere = collect($credentials)->filter(function ($v, $key) {
            return 'password' != $key;
        })->map(function ($value, $filed) { 
            return "`{$filed}` = '{$value}'";
        })->values()->implode(' or ');

        $query->whereRaw($rawWhere);
        return $query->first();
    }
}

2 配置auth.php
'providers' => [
    'users' => [
        'driver' => 'frontend_eloquent',
        'model' => App\User::class,
    ],
...
    
3 在AuthServiceProvider中注册自定义的EloquentUserProvider
public function boot()
{
    $this->registerPolicies();

    Auth::provider('frontend_eloquent', function ($app, $config) {
        return new EloquentUserProvider($app->make('hash'), $config['model']);
    });
}
    
# 这样当使用Auth::guard('web')->attempt()的时候,其中的UserProvider就是我们自定义的了,通过此种方式实现用户登录的控制
# 你还可以配置个后台用户认证表 配置可能长成这样
'guards' => [
    'admin' => [
        'driver' => 'session',
        'provider' => 'admins',
    ],
    
'providers' => [
    'admins' => [
        'driver' => 'backend_eloquent',
        'model' => App\Admin::class,
    ],
# 另外你可能还需要在AuthServiceProvider中再Auth::provider一个全新的EloquentUserProvider 这样你完全可以各类用户拥有完全自定义的认证方式了
# 这样在使用Auth::guard('admin')->attempt()的时候就是你的新的自定义的guard 全新的认证方式了

# 总结一下auth.php中的guards配置表示通过什么样的方式进行用户认证 providers表示用什么方式提供认证的数据
# 每在guards数组中添加一个配置,对应的在使用Auth::guard('name')的时候就会在AuthManager的guards属性下就会多出一组键值对,key是guards中配置的,
# 而value是对应生成的guard实例,默认的auth.php中说明提供session和token两种类型的guard实例,拿session驱动的guard举例,每个SessionGuard实例中
# 都有一个EloquentUserProvider来提供用户数据,我们主要定制的就是这个UserProvider并将其注册到对应的SessionGuard实例中,从而实现定制的用户认证

本文只简单介绍了Auth::attempt的大体思路,大量代码没有提及,感兴趣的可以查看相关文档,提供了大量用户认证的扩展方式。
https://laravel.com/docs/6.x/authentication#adding-custom-guards

感谢各位的观看,我们下期再见

posted @ 2020-12-27 17:00  alwayslinger  阅读(708)  评论(0编辑  收藏  举报