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,未讲解代码可自行查看
# AppHttpControllersAuthLoginController@login
# 登录的主要代码位于IlluminateFoundationAuthAuthenticatesUsers
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 IlluminateHttpRequest $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方法 位于IlluminateAuthCreatesUserProviders
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方法
# IlluminateAuthSessionGuard
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 IlluminateContractsAuthAuthenticatable $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 AppServices;
use IlluminateAuthEloquentUserProvider 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' => AppUser::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' => AppAdmin::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
感谢各位的观看,我们下期再见