1. 什么是Guard
在Laravel/Lumen
框架中,用户的登录/注册的认证根本都曾经封装好了,开箱即用。而登录/注册认证的外围就是:
- 用户的注册信息存入数据库(注销)
- 从数据库中读取数据和用户输出的比照(认证)
上述两步是登录/注册的根本,能够看到都会波及到数据库的操作,这两步框架底层曾经帮咱们做好了,而且思考到了很多状况,比方用户认证的数据表不是user表
而是admin_user
,认证字段是phone
而不是email
,等等一些问题都是Guard
所要解决的,通过Guard
能够指定应用哪个数据表什么字段等,Guard
能非常灵活的构建一套本人的认证体系。
艰深地讲,就是这样:Guard
就像是小区的门卫大叔,冷酷无情,不认人只认注销信息。进小区之前大叔须要先查看你的身份,验证不通过大叔就不让你进去。如果是走路/骑车进去,大叔1须要查看你的门禁卡,他拿出记录了小区所有业主门禁卡信息的本子查看你这个门禁卡信息有没有在这个本子上;如果你开车进去,大叔2就从记录了所有业主车牌号的本子中查看你的车牌号,所以新业次要小区了须要告知门卫大叔们你的门禁卡信息或者车牌号,要不然大叔2不让你进。如果是物业管理员要进小区,门卫大叔3也只认注销信息,管理员出示他的管理员门禁卡,门卫大叔就会查看记录了管理员门禁卡信息的本子。
下面讲的对应了框架中的多用户认证:
- 走路/骑车的人 -> 门禁卡
- 开车的人 -> 车牌号
- 物业管理员 -> 门禁卡
门禁卡和车牌号都是不同的认证形式,而门卫大叔查看的本子就对应了不同数据库中的用户信息,这样讲是不是更容易了解了。
Lumen/Laravel
中以中间件(Middleware
)的形式提供了非常灵活的认证,通过简略的配置就能够切换多个认证。
注:本文所应用的是:Laravel 7.29
2. Guard工作流程
说了这么多,附上一张手工制作的流程图:
从图中能够看到,一个Guard
会波及到三个局部,别离是:
Guard
实现自身User Provider
用户提供者,指定哪个数据表以什么形式获取(eloquent/database
)Authenticatable
接口规定那些货色能够被认证,就是实现它的接口嘛
2. 从配置说起
深刻底层代码之前,先从配置文件讲起。认证的配置次要在/config/auth.php
中,外面能够定义各种认证的门卫大叔(guard):
// /config/auth.php'guards' => [ 'user' => [ 'driver' => 'session', 'provider' => 'users', ], 'admin' => [ 'driver' => 'token', 'provider' => 'admin_users', ],],'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class,// 'table' => 'user' ], 'admin_users' => [ 'driver' => 'eloquent', 'model' => App\Models\AdminUser::class, ],],
配置中定义了两个门卫user
和admin
,driver
字段设置门卫的认证零碎,默认提供两种sessesion
和token
,provider
定义的就是下面说的本子,保留所有的认证用户,provider
上面的drive
定义认证用户如何获取,有两种形式database
和eloquent
形式,个别都是用第二种,model
定义eloquent
形式应用的数据模型,如果driver
是database
,就要设置table
指定数据库表。如果没有代码中没有指定用哪个门卫,就会应用默认的门卫大爷:
'defaults' => [ 'guard' => 'users', 'passwords' => 'users',],
3. 应用Guard例子
咱们以Laravel
中auth
中间件例子来简略说一下:
Route::get('/user/profile', 'UserController@profile')->middleware('auth');
4. 剖析
当发动/user/profile
这个申请时,在进入UserController::profile
办法前,会调用auth
中间件,auth
定义在\app\Http\Kernel.php
中:
// \app\Http\Kernel.phpprotected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, ...];
对应解决脚本是\App\Http\Middleware\Authenticate::class
,
// \app\Http\Middleware\Authenticate.php/*** Handle an incoming request.** @param \Illuminate\Http\Request $request* @param \Closure $next* @param string[] ...$guards* @return mixed** @throws \Illuminate\Auth\AuthenticationException*/public function handle($request, Closure $next, ...$guards){ $this->authenticate($request, $guards); return $next($request);}
Laravel
中中间件的解决入口都是handle
办法,参数中会一数组模式传过来多个应用的guard
,比方这样:
Route::get('/user/profile', 'UserController@profile')->middleware('auth:session,foo,bar');
middleware()
中冒号前后别离是中间件和参数。
handle
办法很简略嘛,就是调用了authenticate()
:
// \Illuminate\Auth\Middleware\Authenticate.php/*** Determine if the user is logged in to any of the given guards.** @param \Illuminate\Http\Request $request* @param array $guards* @return void** @throws \Illuminate\Auth\AuthenticationException*/protected function authenticate($request, array $guards){ if (empty($guards)) { $guards = [null]; } foreach ($guards as $guard) { if ($this->auth->guard($guard)->check()) { return $this->auth->shouldUse($guard); } } $this->unauthenticated($request, $guards);}
authenticate()
办法遍历传过来的guard
,而后check()
,只有满足其中一个,就间接返回,否则就会抛出AuthenticationException
异样。
⚠️留神
$this->auth->guard($guard)->check()
这个是要害,它是通过在auth
属性上链式调用的,咱们来「公众号」(正义的程序猿)一步一步剖析下:
// \Illuminate\Auth\Middleware\Authenticate.phpnamespace Illuminate\Auth\Middleware;use Closure;use Illuminate\Auth\AuthenticationException;use Illuminate\Contracts\Auth\Factory as Auth;use Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests;class Authenticate implements AuthenticatesRequests{ /** * The authentication factory instance. * * @var \Illuminate\Contracts\Auth\Factory */ protected $auth; /** * Create a new middleware instance. * * @param \Illuminate\Contracts\Auth\Factory $auth * @return void */ public function __construct(Auth $auth) { $this->auth = $auth; } ...}
这里的$auth
其实是\Illuminate\Contracts\Auth\Factory
接口的一个实例,通过构造函数注入进来,通过dd($this->auth)
形式发现这个其实就是Illuminate\Auth\AuthManager
实例,它实现了Illuminate\Contracts\Auth\Factory
接口:
// \Illuminate\Contracts\Auth\Factory.phpnamespace Illuminate\Contracts\Auth;interface Factory{ /** * Get a guard instance by name. * * @param string|null $name * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard */ public function guard($name = null); /** * Set the default guard the factory should serve. * * @param string $name * @return void */ public function shouldUse($name);}
这个接口有guard()
办法,所以下面能够间接链式调用。
通过接口定义的申明,咱们能够晓得guard()
返回\Illuminate\Contracts\Auth\Guard
或者\Illuminate\Contracts\Auth\StatefulGuard
这两个接口,具体在AuthManager
中的实现是这样的:
// \Illuminate\Auth\AuthManager.php/*** Attempt to get the guard from the local cache.** @param string|null $name* @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard*/public function guard($name = null){ $name = $name ?: $this->getDefaultDriver(); return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);}
通过咱们在middleware()
中传过来的参数创立对应的guard
实例,没有就是默认driver
对应的guard
,最初check()
。
这节最初讲一下
AuthManager
是什么时候创立的?
Laravel
框架初始化时,很多服务都是以服务提供者(ServiceProvider
)的模式创立的,AuthManager
就是AuthServiceProvider
创立的:
// \Illuminate\Auth\AuthServiceProvider.phpnamespace Illuminate\Auth;class AuthServiceProvider extends ServiceProvider{ /** * Register the service provider. * * @return void */ public function register() { $this->registerAuthenticator(); .... } /** * Register the authenticator services. * * @return void */ protected function registerAuthenticator() { $this->app->singleton('auth', function ($app) { // Once the authentication service has actually been requested by the developer // we will set a variable in the application indicating such. This helps us // know that we need to set any queued cookies in the after event later. $app['auth.loaded'] = true; return new AuthManager($app); }); .... } ....}
AuthServiceProvider
中在注册时调用registerAuthenticator()
,创立auth
单例指向AuthManager
实例。
通过下面的一波剖析,咱们晓得guard
的创立是受AuthManager
治理的,AuthManager
在这里的指摘就是解析driver
并创立guard
。
所以当初整个middleware('auth')
的流程大抵如下:
5. Guard接口
下面说到AuthManager
创立了guard
,而后调用check()
,我先当初来剖析下Guard
。还是那句话,不论下层业务代码如许简单,底层的接口往往是很简略的。Lumen/Laravel
框架中大部分接口被设计成是一种契约(Contracts)
,Guard
也一样的,它的代码在\vendor\illuminate\contracts\Auth\Guard.php
文件中,只有6个办法:
// \Illuminate\Contracts\Auth\Guard.phpnamespace Illuminate\Contracts\Auth;interface Guard{ // 判断以后用户是否登录 public function check(); // 判断以后用户是否是游客(未登录) public function guest(); // 获取以后认证的用户 public function user(); // 获取以后认证用户的 id,严格来说不肯定是 id,应该是这个模型中的主键字段 public function id(); // 用户验证 public function validate(array $credentials = []); // 设置以后认证过的用户 public function setUser(Authenticatable $user);}
很简略,有木有~同样,还有一个StatefulGuard
接口,继承自Guard
接口并加了几个有状态的办法,代表有状态,就是每次申请都带有用户的状态信息比方session
,代码如下:
// Illuminate\Contracts\Auth\StatefulGuard.phpnamespace Illuminate\Contracts\Auth;interface StatefulGuard extends Guard{ // 指定数据验证 public function attempt(array $credentials = [], $remember = false); // 将这一次request验证通过登录,不会保留session/cookie public function once(array $credentials = []); // 登录 public function login(Authenticatable $user, $remember = false); // 应用id登录 public function loginUsingId($id, $remember = false); // 和once()一样,不过是用id public function onceUsingId($id); // 通过remember cookie登录 public function viaRemember(); // 登记 public function logout();}
UML
图大抵如下:
6. Guard接口的相干实现
底层接口着实简略,再来剖析下下层的实现代码,框架中默认实现了几个Guard
,比方Web开发用到的SessionGuard
,接口开发用到的TokenGuard
,这些都实现自\Illuminate\Contracts\Auth
或者\Illuminate\Contracts\Auth\StatefulGuard
,曾经满足咱们日常所需了。
几个Guard
的check()
办法都是一样的,都定义在GuardHelpers
这个Trait
中:
// \Illuminate\Auth\GuardHelpers.php/*** Determine if the current user is authenticated.** @return bool*/public function check(){ return ! is_null($this->user());}
user()
就是在不同的Guard
中实现了,前面也次要看这个办法。
什么是Trait:你能够了解成一系列办法的汇合,就是把常常应用到的反复办法整合起来,在class外面间接use应用,上下文还是援用它的那个class,缩小了反复代码量,而且比class更轻量,不须要new在应用。
6.1 RequestGuard.php
RequestGuard
认证一个http
申请,具体怎么认证,它是通过callback
实现的,认证逻辑在callback
中间接放到了下层让用户自定义,UML
图:
看代码实现也很简略:
// \Illuminate\Auth\RequestGuard.php/*** Get the currently authenticated user.** @return \Illuminate\Contracts\Auth\Authenticatable|null*/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; } return $this->user = call_user_func( $this->callback, $this->request, $this->getProvider() );}
RequestGuard
很多文章都是一笔带过,这【公众号)里我说(正义的程序猿)一下,通常咱们应用不到RequestGuard
,只有在自定义Guard
时才用得上。
应用形式如下
AuthServiceProvider
中注册自定义的guard
,设置名称和callback
:
// App\Providers\AuthServiceProvider.phpuse App\User;use Illuminate\Http\Request;use Illuminate\Support\Facades\Auth;/** * Register any application authentication / authorization services. * * @return void */public function boot(){ $this->registerPolicies(); Auth::viaRequest('custom-token', function ($request) { return User::where('my-token', $request->my_token)->first(); });}
auth.php
中配置自定义guard
'guards' => [ 'my-api' => [ 'driver' => 'custom-token', ],],
- 应用
还是下面的例子:
Route::get('/user/profile', 'UserController@profile')->middleware('auth:my-api');
最初在认证的时候就会间接应用咱们设置的callback
了。
下面viaRequest()
也是定义AuthManager
中:
// \Illuminate\Auth\AuthManager.php/*** Register a new callback based request guard.** @param string $driver* @param callable $callback* @return $this*/public function viaRequest($driver, callable $callback){ return $this->extend($driver, function () use ($callback) { $guard = new RequestGuard($callback, $this->app['request'], $this->createUserProvider()); $this->app->refresh('request', $guard, 'setRequest'); return $guard; });}
6.2 SessionGuard
见名知义,此guard
是基于session
的,个别最罕用的就是(公众号:)这(正义的程序猿)个了。因为是基于session
所以是有状态的,所以这个类定义的时候实现了StatefulGuard
接口,而且加了更多逻辑代码和正文加起来有800+行,
// \Illuminate\Auth\SessionGuard.phpnamespace Illuminate\Auth;use Illuminate\Contracts\Auth\StatefulGuard;use Illuminate\Contracts\Auth\SupportsBasicAuth;class SessionGuard implements StatefulGuard, SupportsBasicAuth{ ...}
UML
图:
用户认证的代码略微简单一点,如下:
// \Illuminate\Auth\SessionGuard.php/*** Get the currently authenticated user.** @return \Illuminate\Contracts\Auth\Authenticatable|null*/public function user(){ if ($this->loggedOut) { return; } // 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; } $id = $this->session->get($this->getName()); // First we will try to load the user using the identifier in the session if // one exists. Otherwise we will check for a "remember me" cookie in this // request, and if one exists, attempt to retrieve the user using that. if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) { $this->fireAuthenticatedEvent($this->user); } // If the user is null, but we decrypt a "recaller" cookie we can attempt to // pull the user data on that cookie which serves as a remember cookie on // the application. Once we have a user we can return it to the caller. if (is_null($this->user) && ! is_null($recaller = $this->recaller())) { $this->user = $this->userFromRecaller($recaller); if ($this->user) { $this->updateSession($this->user->getAuthIdentifier()); $this->fireLoginEvent($this->user, true); } } return $this->user;}
梳理下,大抵是先从session
获取用户的主键id
,而后通过特定的UserProvider
查找用户,查找胜利阐明验证胜利,如果没有,就用recaller
查问用户,这里就是remember token
查找,就是登录时“记住我”的那个选项,remember token
是保留在cookie
当中的,如果remember token
查找胜利,就阐明验证胜利,否则验证失败。
6.3 TokenGuard
TokenGuard
也实现了Guard
接口,实用于无状态的api
认证,UML
图:
因为不要保护状态整个代码就简略很多:
// \Illuminate\Auth\TokenGuard.phpnamespace Illuminate\Auth;use Illuminate\Contracts\Auth\Guard;use Illuminate\Contracts\Auth\UserProvider;use Illuminate\Http\Request;class TokenGuard implements Guard{ ... /** * Get the currently authenticated user. * * @return \Illuminate\Contracts\Auth\Authenticatable|null */ 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; } ...}
先从申请中获取api_token
,再用api_token
从指定的UserProvider
查找api_token
对应的用户信息。
至此,Laravel
中Guard
相干的剖析曾经差不多了,通过剖析它的源码,咱们深刻理解了框架背地的思维,梳理的过程也是学习的过程,对老手而言能疾速把握guard
的相干常识并疾速上手,对老鸟而言,我感觉这篇文章写的曾经很细了,能更好地理解框架背地的精华写出更优雅的代码。
总结
在深刻学习Guard
源码后,理解到底层演绎为两个外围,一是UserProvider
,认证用户数据起源,通常是本地数据库,二是认证逻辑,逻辑这块次要就是Guard
来做了。对于自定义Guard
,下面也略微讲了一点,通过AuthManager
的viaRequest
来做,对于用户数据源咱们也不用拘泥于现有的,咱们也能够将数据源指向redis
或者近程接口,只有实现相干接口,比方这样:
namespace app\Providers;use Illuminate\Contracts\Auth\Authenticatable;use Illuminate\Contracts\Auth\UserProvider;class RedisUserProvider implements UserProvider{ /** * Retrieve a user by their unique identifier. * * @param mixed $identifier * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveById($identifier) { // TODO: 通过id取redis中对应的用户 } ....}
也能够从近程接口获取:
class ApiUserProvider implements UserProvider{ /** * Retrieve a user by their unique identifier. * * @param mixed $identifier * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveById($identifier) { // TODO: 通过id结构curl申请后果 }}
最初,附上一张我在学习过程中总结的UML
图:
文章首发在我本人的博客:
https://xydida.com/2021/2/24/...
欢送大佬们赏光,文章如果有误,还请大家斧正。