功能初始化
php artisan make:auth 使用laravel提供的命令行功能,创建路由、控制器、视图。
路由:
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');
HomeController:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class HomeController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}
/**
* Show the application dashboard.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function index()
{
return view('home');
}
}
视图:
resources\views\auth\
路由分析
Auth::routes();
很明显,这是用facade的形式调用了容器中Auth实例的routes方法。
在config/app.php
的facade别名配置中,找到了:
'Auth' => Illuminate\Support\Facades\Auth::class,
然后查看该Auth门面类,发现其中有routes方法:
class Auth extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'auth';
}
/**
* Register the typical authentication routes for an application.
*
* @param array $options
* @return void
*/
public static function routes(array $options = [])
{
static::$app->make('router')->auth($options);
}
}
可见其使用了router实例的auth方法,那么从哪找这个router呢,我们都知道门面的特点只是替代容器进行访问,所以门面这里还是调用的容器里的实例,所以这个router就是绑定到容器里的服务名称,那么肯定就是在路由模块里的服务提供者里找。
Illuminate\Routing\RoutingServiceProvider
然后通过这里找到了注册进容器时的服务名称就是router,绑定的类是Illuminate\Routing\Router
。
然后从该Router类中找到了auth方法:
/**
* Register the typical authentication routes for an application.
*
* @param array $options
* @return void
*/
public function auth(array $options = [])
{
// Authentication Routes...
$this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
$this->post('login', 'Auth\LoginController@login');
$this->post('logout', 'Auth\LoginController@logout')->name('logout');
// Registration Routes...
if ($options['register'] ?? true) {
$this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
$this->post('register', 'Auth\RegisterController@register');
}
// Password Reset Routes...
if ($options['reset'] ?? true) {
$this->resetPassword();
}
// Email Verification Routes...
if ($options['verify'] ?? false) {
$this->emailVerification();
}
}
由此可见是从这里增加的Auth相关的路由。
中间件底层运作分析
HomeController
的构造方法中定义了该控制器的中间件使用auth,让我们分析一下底层是如何运作的。
public function __construct()
{
$this->middleware('auth');
}
HomeController继承的App\Http\Controllers\Controller
,而该Controller又继承的Illuminate\Routing\Controller
这个抽象类。
从最终的这个抽象类中看到调用的这个middleware方法:
/**
* Register middleware on the controller.
*
* @param array|string|\Closure $middleware
* @param array $options
* @return \Illuminate\Routing\ControllerMiddlewareOptions
*/
public function middleware($middleware, array $options = [])
{
foreach ((array) $middleware as $m) {
$this->middleware[] = [
'middleware' => $m,
'options' => &$options,
];
}
return new ControllerMiddlewareOptions($options);
}
/**
* Get the middleware assigned to the controller.
*
* @return array
*/
public function getMiddleware()
{
return $this->middleware;
}
可以理解为,给当前使用的这个控制器,也就是HomeController
,设定了要使用的中间件。
上边这里不仅有一个middleware
方法用来设置控制器的中间件,还有一个getMiddleware
方法来获取控制器的中间件,请注意这个getMiddleware
方法最后会调用到。
那么这个中间件从什么时候触发的呢?这里简单列一下一个请求的生命流程在Route时经历的方法:
Illuminate\Foundation\Http\Kernel->handle // http请求处理
Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter // 基础服务的注册、使用管道经过基础中间件,然后返回管道中then方法的响应结果。
Illuminate\Foundation\Http\Kernel->dispatchToRouter // 交给路由模块处理,并返回路由模块处理结果
Illuminate\Routing\Router->dispatch // 对Router类的当前要处理的请求的初始化,并返回dispatchToRoute方法处理结果
Illuminate\Routing\Router->dispatchToRoute // 匹配到对应路由,并返回runRoute方法执行对该路由的处理结果。
Illuminate\Routing\Router->runRoute // 使用prepareResponse方法处理runRouteWithinStack方法的返回结果,并返回处理结果。
Illuminate\Routing\Router->runRouteWithinStack // 真正运行对该路由的处理
从下面的runRouteWithinStack
方法可以看出,在这里又使用了管道。
这里首先判断系统是否关闭了中间件,如果没有关闭就获取处理这个请求的这个路由的所有中间件。
然后使用管道进行处理,经过一层层中间件,最后执行$route->run()
方法,该run方法会判断路由类型是控制器还是闭包,然后获取处理结果并return。
/**
* Run the given route within a Stack "onion" instance.
*
* @param \Illuminate\Routing\Route $route
* @param \Illuminate\Http\Request $request
* @return mixed
*/
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()
);
});
}
然后我们看gatherMiddleware
这个方法,这个方法就是获取本次请求所匹配的那个路由的所有中间件。
从下面代码可见,首先调用gatherMiddleware
方法获取本次要使用的中间件,由于获取的中间件是别名,所以还要从所有的中间件和组中进行匹配,得到别名所对应的类。
/**
* Gather the middleware for the given route with resolved class names.
*
* @param \Illuminate\Routing\Route $route
* @return array
*/
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);
}
那么就分析一下gatherMiddleware
的代码,其中$this->controllerMiddleware()
这个方法就是获取了在路由里面设置的中间件,也就是我们从HomeController
中设置的那个auth中间件。
/**
* Get all middleware, including the ones from the controller.
*
* @return array
*/
public function gatherMiddleware()
{
if (! is_null($this->computedMiddleware)) {
return $this->computedMiddleware;
}
$this->computedMiddleware = [];
return $this->computedMiddleware = array_unique(array_merge(
$this->middleware(), $this->controllerMiddleware()
), SORT_REGULAR);
}
再看controllerMiddleware
方法,这块用了controller调度器的一个类,调用了该调度器类的getMiddleware
方法。
/**
* Get the middleware for the route's controller.
*
* @return array
*/
public function controllerMiddleware()
{
if (! $this->isControllerAction()) {
return [];
}
return $this->controllerDispatcher()->getMiddleware(
$this->getController(), $this->getControllerMethod()
);
}
然后再看上面调度器类中的getMiddleware
方法,这就很明显了,这里用处理该路由的控制器,也就是HomeController
去调用其父类Illuminate\Routing\Controller
这个抽象类的getMiddleware
方法。
/**
* Get the middleware for the controller instance.
*
* @param \Illuminate\Routing\Controller $controller
* @param string $method
* @return array
*/
public function getMiddleware($controller, $method)
{
if (! method_exists($controller, 'getMiddleware')) {
return [];
}
return collect($controller->getMiddleware())->reject(function ($data) use ($method) {
return static::methodExcludedByOptions($method, $data['options']);
})->pluck('middleware')->all();
}
截止这里,一个匹配该路由的HTTP请求,就获取到了控制器中设置的中间件,然后一层一层回到上面的runRouteWithinStack
方法处,然后利用管道执行使中间件生效。
auth中间件执行流程分析
中间件的添加和删除是在App\Http\Kernel
类中定义的,这些中间件在框架初始化时就配置到了Router类中,具体可见Illuminate\Foundation\Http\Kernel
类的构造方法。
从中间件的别名对应关系可以发现,auth中间件对应的类是
'auth' => \App\Http\Middleware\Authenticate::class,
然后从第三步分析中,在runRouteWithinStack
方法这里,使用管道让请求经过了每个中间件,这里打印一下$middleware
的值:
/**
* Run the given route within a Stack "onion" instance.
*
* @param \Illuminate\Routing\Route $route
* @param \Illuminate\Http\Request $request
* @return mixed
*/
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
$middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);
dd($middleware);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});
}
dd调试输出内容为:
array:7 [▼
0 => "App\Http\Middleware\EncryptCookies"
1 => "Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse"
2 => "Illuminate\Session\Middleware\StartSession"
3 => "Illuminate\View\Middleware\ShareErrorsFromSession"
4 => "App\Http\Middleware\VerifyCsrfToken"
5 => "App\Http\Middleware\Authenticate"
6 => "Illuminate\Routing\Middleware\SubstituteBindings"
]
然后看管道中then方法,执行这个管道时其中$middleware
的运行情况:
/**
* Run the pipeline with a final destination callback.
*
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination)
{
dump(array_reverse($this->pipes));
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
其中第一个参数就是以上打印的数组,第二个参数$this->carry()
是一个闭包,是要迭代处理数组值的方法。最后一个参数是作为第一次迭代的初始值。
也就是说利用了array_reduce
函数,就开始了对中间件的处理。
请注意,这个$this->carry()
方法返回的也是闭包,也没有真正执行,而是最后一行这里:return $pipeline($this->passable);
才执行了所有闭包。
下边贴上了最终执行管道闭包时的代码:
$response = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
这段代码也很好理解,pipe是每一节管道,也就是每个中间件,$this->method是要执行的中间件中的方法,默认是handle。
执行auth中间件的时候,在这里就可以理解为:
$response = App\Http\Middleware\Authenticate->handle(...);
那么我们看看Authenticate类的handle方法的实现逻辑,在Authenticate父类中:
handle方法没什么好说的,直接看authenticate方法,可见,如果不通过if ($this->auth->guard($guard)->check()) {
的判断,就会判定该请求未授权。
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
/**
* 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);
}
}
throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);
}
这个$this->auth
是Illuminate\Auth\AuthManager
类的门面,那我们看AuthManager
类中的guard方法:
该方法的作用是返回一个守卫,首先判断如果传递的守卫如果是null,就使用getDefaultDriver方法获取默认守卫。
然后调用resolve方法把守卫实例解析出来,在resolve中会根据配置的驱动调用对应处理方法,根据配置文件的默认项,最终调用createSessionDriver方法。
/**
* 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);
}
/**
* Get the default authentication driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['auth.defaults.guard'];
}
/**
* Resolve the given guard.
*
* @param string $name
* @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
*
* @throws \InvalidArgumentException
*/
protected function resolve($name)
{
$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';
if (method_exists($this, $driverMethod)) {
return $this->{$driverMethod}($name, $config);
}
throw new InvalidArgumentException(
"Auth driver [{$config['driver']}] for guard [{$name}] is not defined."
);
}
/**
* Create a session based authentication guard.
*
* @param string $name
* @param array $config
* @return \Illuminate\Auth\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;
}
在createSessionDriver
方法中,根据配置文件创建了provider和SessionGuard守卫实例,并对守卫实例进行一些初始化设置,并返回一个SessionGuard
实例。
为了后面对照查看,这里贴上config/auth.php的部分配置:
config/auth.php的部分配置:
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
],
然后再回到上面中间件的authenticate方法中,使用得到的SessionGuard
实例调用check方法,该check方法位于SessionGuard类内use的trait类GuardHelpers中。
通过check方法检测身份是否通过,如果通过则调用shouldUse方法设定通过审核的守卫以及对应的用户实例(用户实例就是上面配置文件中providers中的model,也就是App\User::class)。
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
截止这里,这个auth中间件的流程就走完了。
另外顺带一提,登录成功后通过门面方式比较常用的比如:\Auth::user();就是调用的SessionGuard方法的user方法。
登录流程分析
先看看未登录访问处理流程
首先,guard防护类型在config/auth.php中默认是web,另外默认防护类型还支持api。
驱动也是支持session和token这两种,在auth组件中的Illuminate\Auth\AuthManager
类中都可以看到对应的处理方法。
本次使用的就是默认的配置。
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
],
],
接下来访问/home路由,由于HomeController
设定了auth中间件,route模块在执行管道时,会经过Illuminate\Auth\Middleware\Authenticate
这个负责认证的中间件处理。
Authenticate:
/**
* 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);
}
/**
* 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);
}
}
throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);
}
guard防护类型,默认的话就会取设定的config值,也就是web,驱动就是session,服务提供者为users,提供者的驱动是orm,model使用的User。
上面这个Authenticate中间件类中,$this->auth
是AuthManager类。
1.首先调用grand方法设置好要使用的防护类型。
2.然后调用check方法检测用户是否已登录,如果未登录则返回null。
3.如果第二步返回null,则抛出AuthenticationException
类,其中redirectTo()
就是获取我们在子类中设定的未授权跳转地址。
如果走到了第三步,route组件的的pipeline会catch到中间件抛出的异常,处理后return,浏览器收到response后,根据redirectTo()返回的URL,跳转到/login页面,整个请求结束。
再看看处理登录请求的流程
填写账号密码提交表单后,根据框架默认的Auth::route()
,对应的Controller是Auth\LoginController@login
,LoginController use 了trait类AuthenticatesUsers,很多逻辑都是在该trait类中判断的,所以下面的代码基本都是以这个trait类展开。
分析处理登录请求的方法,该方法说明非常明确,处理一个向本应用发起的登录请求。
AuthenticatesUsers:
/**
* Handle a login request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*
* @throws \Illuminate\Validation\ValidationException
*/
public function login(Request $request)
{
// 1.第一步调用validateLogin方法验证表单必填项和是否为字符串。
// 注:这里底层查了一下request类并没有validate方法,
// 而是在服务提供者Illuminate\Foundation\Providers\FoundationServiceProvider中使用macro方法注册的,
// 该macro方法属于Macroable这个trait类,该方法用来给已定义无法修改的对象提供一种额外方式(增加方法)进行功能增强。
$this->validateLogin($request);
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
// the IP address of the client making these requests into this application.
// 2.第二步是登录时的特征检测,比如可以验证是否登录太多次,或者根据IP禁用登录等规则。这个实现是在ThrottlesLogins(风控)类中。
// 但是可以在LoginController中根据自己的业务重写hasTooManyLoginAttempts方法,对登录的用户进行验证。
// 当检测方法通过时,会调用fireLockoutEvent方法对该用户进行锁定或者其他业务操作,这个方法也可以在LoginController中重写。sendLockoutResponse方法也是一样。
if (method_exists($this, 'hasTooManyLoginAttempts') &&
$this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
// 3.第三步,调用attemptLogin方法登录用户,$this->guard()是根据auth配置初始化得到的 SessionGuard (session防护类)。
// 由于attemptLogin内调用的是SessionGuard防护类的方法,所以下面增加一个代码块用于分析SessionGuard防护类的逻辑。
if ($this->attemptLogin($request)) {
// 通过检测,这时SessionGuard里的$user变量已经存储了当前登录用户的model 对象。并将用户导向登录成功后的URL。
// 也就是说这个http响应给客户端之后,客户端再访问本应用,即为已登录身份。
// 因为验证的时候,会在SessionGuard中寻找$user变量,该变量值已被设置,所以中间件就会放行。
return $this->sendLoginResponse($request);
}
// 4.记录一次登录失败,用于风控检测用户尝试登录次数。
// If the login attempt was unsuccessful we will increment the number of attempts
// to login and redirect the user back to the login form. Of course, when this
// user surpasses their maximum number of attempts they will get locked out.
$this->incrementLoginAttempts($request);
// 5.若第三步得到false,登录失败,则在这里调用sendFailedLoginResponse方法抛出登录失败消息。
return $this->sendFailedLoginResponse($request);
}
/**
* 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')
);
}
SessionGuard:
/**
* Attempt to authenticate a user using the given credentials.
*
* @param array $credentials
* @param bool $remember
* @return bool
*/
public function attempt(array $credentials = [], $remember = false)
{
$this->fireAttemptEvent($credentials, $remember);
// 根据config,这里的provider是EloquentUserProvider,返回值是用User model对象。
$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
// If an implementation of UserInterface was returned, we'll ask the provider
// to validate the user against the given credentials, and if they are in
// fact valid we'll log the users into the application and return true.
// 检测凭证和上面查询出来的对象密码是否吻合,如果吻合则调用login方法,
// login方法中使用setUser方法,将对象赋值给本类的$user变量。
if ($this->hasValidCredentials($user, $credentials)) {
$this->login($user, $remember);
return true;
}
// If the authentication attempt fails we will fire an event so that the user
// may be notified of any suspicious attempts to access their account from
// an unrecognized user. A developer may listen to this event as needed.
$this->fireFailedEvent($user, $credentials);
return false;
}
/**
* Log a user into the application.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param bool $remember
* @return void
*/
public function login(AuthenticatableContract $user, $remember = false)
{
$this->updateSession($user->getAuthIdentifier());
// If the user should be permanently "remembered" by the application we will
// queue a permanent cookie that contains the encrypted copy of the user
// identifier. We will then decrypt this later to retrieve the users.
if ($remember) {
$this->ensureRememberTokenIsSet($user);
$this->queueRecallerCookie($user);
}
// If we have an event dispatcher instance set we will fire an event so that
// any listeners will hook into the authentication events and run actions
// based on the login and logout events fired from the guard instances.
$this->fireLoginEvent($user, $remember);
$this->setUser($user);
}
/**
* Set the current user.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return $this
*/
public function setUser(AuthenticatableContract $user)
{
$this->user = $user;
$this->loggedOut = false;
$this->fireAuthenticatedEvent($user);
return $this;
}
以上就是对登录流程的分析,在Auth模块的设计中很好的体现了trait类特性的运用(本类中的属性和方法都会覆盖trait类中的属性和方法),可以在子类(LoginController)中重写trait类AuthenticatesUsers内的方法,替换为自己的业务逻辑,非常的方便!
总结
首先就是需要具备一定PHP知识,例如容器、管道、门面、trait等。
要一直学习框架中优秀的东西,不能只做一个应用者,要识其原理,才能化为自己的内功,在实际中运用。