Laravel Authentication
laravel5.5的文档中的Authentication章节,提到php artisan make:auth
命令,执行后,会自动拥有一个注册、登陆、保持登陆状态、未登陆用户跳转登陆页面这些功能。有点好奇他的验证用户流程,翻了下他的源码。
命令执行后web.php中多了如下代码:
Auth::routes(); Route::get('/home', 'HomeController@index')->name('home');
Auth::routes();
最终调用到vendor/laravel/framework/src/Illuminate/Routing/Router.php
的auth()方法,里面只是添加了一些注册,登陆等路由。
Route::get('/home', 'HomeController@index')->name('home');
添加home路由,看看HomeController的构造函数如下
public function __construct() { $this->middleware('auth'); }
好,接下来的故事都从这里开始。
这里肯定是给控制器添加了一个中间件,那么:
- auth具体是什么?
- 这个中间件是如何工作?
middleware方法是父类Controller中的,里面能看到有一个$middleware数组,也就是说每个控制器都会有一个这样的中间件数组。这里执行middleware方法只是将auth这个字符串添加了到$middleware数组中。
既然是控制器自身的中间件,那么这个中间件执行的时候肯定是该控制(或者对应路由)被访问执行的时候,接下来去找一下控制器执行的地方。
Illuminate/Foundation/Http/Kernel.php
中下面的代码显示请求进来的时候会先通过Kernel的中间件($this->middleware),然后才会dispatchToRouter。
protected function sendRequestThroughRouter($request) { // ... return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); }
顺着dispatchToRouter会看到他更加request来找到对应路由,然后执行路由,最终会走到Illuminate/Routing/Router.phpd的runRouteWithinStack方法。这里能看到Kernel走过的逻辑这里又来了一遍,在执行路由之前先过一遍路由的中间件。
protected function runRouteWithinStack(Route $route, Request $request) { $shouldSkipMiddleware = $this->container->bound('middleware.disable') && $this->container->make('middleware.disable') === true; $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route); return (new Pipeline($this->container)) ->send($request) ->through($middleware) ->then(function ($request) use ($route) { return $this->prepareResponse( $request, $route->run() ); }); }
那么接下来看看中间件是怎么执行的。
// Pipeline.php public function send($passable) { $this->passable = $passable; return $this; } public function through($pipes) { $this->pipes = is_array($pipes) ? $pipes : func_get_args(); return $this; } public function then(Closure $destination) { $pipeline = array_reduce( array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination) ); return $pipeline($this->passable); }
send设置passable为$request,through设置pipes为中间件数组,这里的中间件是通过$this->gatherRouteMiddleware($route)
来合并的route的中间件以及控制器的中间件。
public function gatherRouteMiddleware(Route $route) { $middleware = collect($route->gatherMiddleware())->map(function ($name) { return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups); })->flatten(); return $this->sortMiddleware($middleware); }
我原本以为auth中间件的实例化肯定是在之前什么地方被绑定好了,然后在实际要开始执行的时候(也就是洋葱模型开始一层层执行的时候)通过服务容器实例化之类的。但是,我发现在经过MiddlewareNameResolver::resolve这里后,auth字符串进去,出来了一个Illuminate\Auth\Middleware\Authenticate。
public static function resolve($name, $map, $middlewareGroups) { // ... return ($map[$name] ?? $name).(! is_null($parameters) ? ':'.$parameters : ''); }
也就是说$map数组里面有设置auth => Illuminate\Auth\Middleware\Authenticate
,而$map是Router里面的$this->middleware
,然后我才在Kernel里面找到了这个映射
// app/Http/Kernel.php protected $routeMiddleware = [ 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, // ... ];
看到$routeMiddleware,感觉很明了了,那么看看kernel里的这个在哪里设置到Router里去的。
答案是在Kernel父类里的构造函数里通过aliasMiddleware设置的。
// Illuminate/Foundation/Http/Kernel.php public function __construct(Application $app, Router $router) { $this->app = $app; $this->router = $router; $router->middlewarePriority = $this->middlewarePriority; foreach ($this->middlewareGroups as $key => $middleware) { $router->middlewareGroup($key, $middleware); } foreach ($this->routeMiddleware as $key => $middleware) { $router->aliasMiddleware($key, $middleware); } } // Illuminate/Routing/Router.php public function aliasMiddleware($name, $class) { $this->middleware[$name] = $class; return $this; }
到这里,解决了一个问题:HomeController构造函数里设置的auth中间件是Illuminate\Auth\Middleware\Authenticate。
中间件的洋葱模型是在pipeline的then里通过reduce和carry方法来实现的,这里要注意的是,Router里用到Pipeline是Illuminate/Routing/Pipeline.php
,他重写了carry方法,并且复用了整个父类的逻辑,跟父类不同的是,他套上了try catch,当中间件执行中出错时,这里会捕获到。在父类的carry中会通过make来实例化auth,并且当开始执行的时候,会执行每个中间件的handle方法,因此接下来看看auth中间件干了什么,是如何验证用户的,从Illuminate\Auth\Middleware\Authenticate->handle()开始。
// Illuminate\Auth\Middleware\Authenticate public function handle($request, Closure $next, ...$guards) { $this->authenticate($guards); return $next($request); }
这里的逻辑我觉得有点奇怪的是,执行了authenticate方法后,就开始调用next了,我原本以为会是判断一下authenticate的结果,成功才转next,否则redirect。
protected function authenticate(array $guards) { if (empty($guards)) { return $this->auth->authenticate(); } // ... }
这里的$this->auth是通过依赖注入进来的,大致说下依赖注入流程,服务容器会通过make来解决依赖问题,make首先会检查aliase,在registerCoreContainerAliases中的那个数组可以看到\Illuminate\Contracts\Auth\Factory对应一个叫做auth的别名。
use Illuminate\Contracts\Auth\Factory as Auth; public function __construct(Auth $auth) { $this->auth = $auth; } // 'auth'=> [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class]
现在又有一个同样的问题,这里的auth是什么?
之前看服务容器的内容的时候,知道config.providers里的provider都会挨个执行里面的register方法。
[ 'providers' => [Illuminate\Auth\AuthServiceProvider::class] ]
AuthServiceProvider里面registerAuthenticator就绑定了一个叫做auth的单例。
所以auth就是AuthManager。
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); }); $this->app->singleton('auth.driver', function ($app) { return $app['auth']->guard(); }); }
所以当前的问题就是看AuthManager->authenticate()干了什么
但是AuthManager里面其实并没有authenticate这个方法,但是他有一个__call()方法,所以AuthManager->authenticate()其实是$this->guard()->authenticate()
public function __call($method, $parameters) { return $this->guard()->{$method}(...$parameters); }
那么先看下guard()会返回个什么对象。
public function guard($name = null) { $name = $name ?: $this->getDefaultDriver(); return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name); } public function getDefaultDriver() { /* 'defaults' => [ 'guard' => 'web', 'passwords' => 'users', ], */ return $this->app['config']['auth.defaults.guard']; }
执行resolve('web')
protected function resolve($name) { $config = $this->getConfig($name); /* [ 'driver' => 'session', 'provider' => 'users', ] */ 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'; // createSessionDriver if (method_exists($this, $driverMethod)) { return $this->{$driverMethod}($name, $config); } throw new InvalidArgumentException("Auth guard driver [{$name}] is not defined."); } protected function getConfig($name) { /* 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'token', 'provider' => 'users', ], ], */ return $this->app['config']["auth.guards.{$name}"]; }
所以看下$this->createSessionDriver(),$provider是一个EloquentUserProvider对象,然后创建并返回了一个SessionGuard对象。
public function createSessionDriver($name, $config) { $provider = $this->createUserProvider($config['provider'] ?? null); $guard = new SessionGuard($name, $provider, $this->app['session.store']); // When using the remember me functionality of the authentication services we // will need to be set the encryption instance of the guard, which allows // secure, encrypted cookie values to get generated for those cookies. if (method_exists($guard, 'setCookieJar')) { $guard->setCookieJar($this->app['cookie']); } if (method_exists($guard, 'setDispatcher')) { $guard->setDispatcher($this->app['events']); } if (method_exists($guard, 'setRequest')) { $guard->setRequest($this->app->refresh('request', $guard, 'setRequest')); } return $guard; } public function createUserProvider($provider = null) { if (is_null($config = $this->getProviderConfiguration($provider))) { return; } /* $config = [ 'driver' => 'eloquent', 'model' => App\User::class, ] */ // ... 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." ); } }
那么先看下guard()会返回个什么对象。
是一个SessionGuard对象,所以AuthManager->authenticate()==$this->guard()->authenticate()==SessionGuard->authenticate()
SessionGuard中并没有authenticate这个方法,这个方法是通过GuardHelpers这个trait引入的。
// Illuminate/Auth/GuardHelpers.php public function authenticate() { if (! is_null($user = $this->user())) { return $user; } throw new AuthenticationException; }
可以看到,他验证的逻辑其实很简单,如果user存在就返回user,不存在就抛出AuthenticationException错误。这个this是SessionGuard所以,调用的是SessionGuard->user()方法。
// Illuminate/Auth/SessionGuard.php public function 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)) { if ($this->user = $this->provider->retrieveById($id)) { $this->fireAuthenticatedEvent($this->user); } } // ... return $this->user; }
逻辑很清晰,先从session中拿到用户id,然后通过provider也就是EloquentUserProvider去找这个id的用户信息。
🙋**$this->session是什么,从哪里来的呢?**
是createSessionDriver中new SessionGuard($name, $provider, $this->app['session.store'])
传入的$this->app['session.store'],那么session.store肯定又是哪个谁的别名,继续想到app.providers。里面果然有一个Illuminate\Session\SessionServiceProvider,所以session.store就是SessionManager->driver()
。
public function register() { $this->registerSessionManager(); $this->registerSessionDriver(); $this->app->singleton(StartSession::class); } protected function registerSessionManager() { $this->app->singleton('session', function ($app) { return new SessionManager($app); }); } protected function registerSessionDriver() { $this->app->singleton('session.store', function ($app) { // First, we will create the session manager which is responsible for the // creation of the various session drivers when they are needed by the // application instance, and will resolve them on a lazy load basis. return $app->make('session')->driver(); }); }
driver方法是SessionManager父类Manager中的,默认会调用createFileDriver来创建,也就是说默认情况下session是保存在文件中的,FileSessionHandler第二参数是文件路径,是读的配置,值是'files' => storage_path('framework/sessions')
,所以默认session是保存在storage/framework/sessions,可以去这个目录下证实一下。
public function driver($driver = null) { $driver = $driver ?: $this->getDefaultDriver(); // $driver = 'file' if (! isset($this->drivers[$driver])) { $this->drivers[$driver] = $this->createDriver($driver); } return $this->drivers[$driver]; } protected function createFileDriver() { return $this->createNativeDriver(); } protected function createNativeDriver() { $lifetime = $this->app['config']['session.lifetime']; return $this->buildSession(new FileSessionHandler( $this->app['files'], $this->app['config']['session.files'], $lifetime )); } protected function buildSession($handler) { if ($this->app['config']['session.encrypt']) { return $this->buildEncryptedSession($handler); } return new Store($this->app['config']['session.cookie'], $handler); }
$this->session是什么,从哪里来的呢?
$id = $this->session->get($this->getName());
是一个通过buildSession创建的Store对象,get也就是从attributes里面去找键值对
// Illuminate/Session/Store.php public function get($key, $default = null) { return Arr::get($this->attributes, $key, $default); }
🙋那么attributes是什么?从哪里设置的?
// Illuminate/Session/Store.php public function start() { $this->loadSession(); if (! $this->has('_token')) { $this->regenerateToken(); } return $this->started = true; } protected function loadSession() { $this->attributes = array_merge($this->attributes, $this->readFromHandler()); }
上面看到attributes有通过loadSession来赋值,先从这里下手找找。回过头去看看SessionServiceProvider,里面还绑定了一个StartSession是一个中间件,而在Kernel中middlewareGroups也有这个
$this->app->singleton(StartSession::class); protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Session\Middleware\StartSession::class, // ... ], ];
StartSession中间件执行的时候调用到了$session->start(),这个$session其实就是上面创建的Store。
// \Illuminate\Session\Middleware\StartSession protected function startSession(Request $request) { return tap($this->getSession($request), function ($session) use ($request) { $session->setRequestOnHandler($request); $session->start(); }); }
调用$session->start(),然后loadSession() 被调用,$this->attributes被赋值。那么重点是就看readFromHandler是从怎么读到的数据,handler是FileSessionHandler,下面的代码大概是根据一个id从FileSessionHandler里面读去数据,然后进行一下反序列化之类的。
protected function readFromHandler() { if ($data = $this->handler->read($this->getId())) { $data = @unserialize($this->prepareForUnserialize($data)); if ($data !== false && ! is_null($data) && is_array($data)) { return $data; } } return []; } public function getId() { return $this->id; }
🙋那么问题又来了,id是啥?
public function setId($id) { $this->id = $this->isValidId($id) ? $id : $this->generateSessionId(); } protected function generateSessionId() { return Str::random(40); }
刚好下面就有一个setId,所以推测是从这里来设置的,可是创建Store的时候并没有传进来id,那么按这里的逻辑,没每次都会随机创建一个id,那岂不是每次id都不一样,这样显然不对劲。
答案在上面startSession里面调用了$this->getSession($request)
,在这里调用了setId,数据是从cookie中来的,这样就很合理了。
public function getSession(Request $request) { return tap($this->manager->driver(), function ($session) use ($request) { $session->setId($request->cookies->get($session->getName())); }); }
那么到目前为止,总结一下:
当一个请求进来的时候,比如这里访问/home,先经过Kernel层级的中间件,然后根据路径将请求分发到对应的路由,找到路由后,先经过路由层级的中间件和控制器上的中间件。对应到的情况就是\Illuminate\Auth\Middleware\Authenticate通过从cookie中获取到的id,去保存在文件中的session数据中去找到用户id,然后去数据库中找user数据,有则返回用户,无则抛出AuthenticationException错误。
🙋没有找到用户只是抛出了错误而已,并没有见到他是如何redirect的啊
这个答案在carry中,
中间件的洋葱模型是在pipeline的then里通过reduce和carry方法来实现的,这里要注意的是,Router里用到Pipeline是
Illuminate/Routing/Pipeline.php
,他重写了carry方法,并且复用了整个父类的逻辑,跟父类不同的是,他套上了try catch,当中间件执行中出错时,这里会捕获到。
protected function carry() { return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { try { $slice = parent::carry(); $callable = $slice($stack, $pipe); return $callable($passable); } catch (Exception $e) { return $this->handleException($passable, $e); } catch (Throwable $e) { return $this->handleException($passable, new FatalThrowableError($e)); } }; }; } protected function handleException($passable, Exception $e) { if (! $this->container->bound(ExceptionHandler::class) || ! $passable instanceof Request) { throw $e; } $handler = $this->container->make(ExceptionHandler::class); $handler->report($e); $response = $handler->render($passable, $e); if (method_exists($response, 'withException')) { $response->withException($e); } return $response; }
从上面看出抛出的错误会交给一个ExceptionHandler去处理,这个玩意在bootstrap阶段就绑定了。
// bootstrap/app.php $app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class ); public function render($request, Exception $e) { // .... $e = $this->prepareException($e); if ($e instanceof HttpResponseException) { return $e->getResponse(); } elseif ($e instanceof AuthenticationException) { return $this->unauthenticated($request, $e); } elseif ($e instanceof ValidationException) { return $this->convertValidationExceptionToResponse($e, $request); } //... } protected function unauthenticated($request, AuthenticationException $exception) { return $request->expectsJson() ? response()->json(['message' => $exception->getMessage()], 401) : redirect()->guest(route('login')); }
这里很明确,如果是AuthenticationException,就通过unauthenticated执行redirect到login路由。
然后还有注意到一个事情就是,浏览器中保存的session信息,或者说sessionId,跟服务器中保存的并不一样,所以服务器拿到session数据后,应该还存在一个解密之类的过程。那么
🙋session信息是如何解密的?
首先这块的处理肯定是有中间件完成的,加上session、解密,就这两个关键词,很容易找到了EncryptCookies这个中间件,最终是通过$this->encrypter->decrypt()
这个方法来完成解密的。
public function handle($request, Closure $next) { return $this->encrypt($next($this->decrypt($request))); } protected function decryptCookie($name, $cookie) { return is_array($cookie) ? $this->decryptArray($cookie) : $this->encrypter->decrypt($cookie, static::serialized($name)); }
那么$this->encrypter是什么?是通过依赖注入,引入了一个EncrypterContract,通过查看Application中设置的别名,知道应该对应一个叫做别名为encrypter的对象。
public function __construct(EncrypterContract $encrypter) { $this->encrypter = $encrypter; } // 'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class]
encrypter是在EncryptionServiceProvider中绑定的,所以encrypter就是Illuminate/Encryption/Encrypter.php
namespace Illuminate\Encryption; public function register() { $this->app->singleton('encrypter', function ($app) { $config = $app->make('config')->get('app'); // If the key starts with "base64:", we will need to decode the key before handing // it off to the encrypter. Keys may be base-64 encoded for presentation and we // want to make sure to convert them back to the raw bytes before encrypting. if (Str::startsWith($key = $this->key($config), 'base64:')) { $key = base64_decode(substr($key, 7)); } return new Encrypter($key, $config['cipher']); }); }
所以要找的decrypt()解密函数如下,可以看到用到了base64,openssl_decrypt,还有json,所以浏览器中保存的session信息,虽然是一串字符串,其实是json转换过去的,里面包含了很多别的信息,不光是sessionId而已。
public function decrypt($payload, $unserialize = true) { $payload = $this->getJsonPayload($payload); $iv = base64_decode($payload['iv']); // Here we will decrypt the value. If we are able to successfully decrypt it // we will then unserialize it and return it out to the caller. If we are // unable to decrypt this value we will throw out an exception message. $decrypted = \openssl_decrypt( $payload['value'], $this->cipher, $this->key, 0, $iv ); if ($decrypted === false) { throw new DecryptException('Could not decrypt the data.'); } return $unserialize ? unserialize($decrypted) : $decrypted; } protected function getJsonPayload($payload) { $payload = json_decode(base64_decode($payload), true); // .... return $payload; }