3rsh1
/single dog/college student/ctfer
3rsh1's Blog

lavarel专题1

放假之前就像做个lavarel反序列化漏洞的专题,但是一直在忙考试,没有时间。现在终于可以从头开始搞一下了。

lavarel基础

首先要清楚lavarel是什么, Laravel是一套简洁、优雅的PHP Web开发框架(PHP Web Framework)(摘自百度),其实就是一个web框架。

1. 基础知识

1.1 命名空间

1.1.1 概述

命名空间是一种封装事务的方法,就像文件系统中的目录差不多。例如在test目录下有若干文件,那么对于test目录下的文件来说,test就是一个命名空间。同一个文件可以放在不同的目录下,但是同一个目录下不能有相同的两个文件。

为什么要使用命名空间呢?

我们的代码可能和其他开发者的代码使用相同的类名、接口名、函数或常量名,如果不使用命名空间,名称会起冲突,导致PHP执行出错。而使用命名空间将代码放到唯一的厂商命名空间,我们的代码就可以和其他开发者使用相同的类名、接口名、函数或常量名。

1.1.2 声明和调用

任意合适的php代码都可以放在命名空间内,包括但是不限于类,函数,接口,变量等。 命名空间通过关键字namespace 来声明。如果一个文件中包含命名空间,它必须在其它所有代码之前声明命名空间,除了一个以外:declare关键字,任意字符都不能出现在声明命名空间之前,也就是说声明命名空间必须是第一句话。

<?php
namespace MyProject;

const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */  }

?>

对于子命名空间的声明其实和目录的分级差不多,例如声明一个子命名空间:

<?php
namespace MyProject\Sub\Level;

const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */  }

?>

我们这里看到,这个子命名空间有一个上级的命名空间,而且名字之间是用反斜杠分开的。若是在同一个文件中声明多个子命名空间可以有两种表示方式:第一种是不做任何处理,即直接紧接着声明即可,第二种是将声明的子命名空间里的内容用{}给分别括起来。这里就不举例子了。

那么该如何访问一个命名空间中的内容呢?依旧是类比于文件系统访问文件的方法:

  1. foo.txt表示当前目录下的foo文件。
  2. test/foo.txt表示当前目录下的test目录中的foo文件,以上两种都是相对路径表示。
  3. /test/foo.txt这是绝对路径的表示。

访问命名空间的元素的方法其实也是类似的,

  1. 非限定名称:$a=new foo();表示的是当前命名空间下的foo类,如果当前文件并没有设置命名空间的话,foo会被认为是一个全局的类。 如果命名空间中的函数或常量未定义,则该非限定的函数名称或常量名称会被解析为全局函数名称或常量名称。
  2. 限定名称,或包含前缀的名称,例如 $a = new subnamespace\foo(); foo 会被解析为 currentnamespace\subnamespace\foo
  3. 完全限定名称,或包含了全局前缀操作符的名称,例如, $a = new \currentnamespace\foo();

其实如果导入某个命名空间的话,使用命名空间中的元素会更加方便。实际上是支持通过别名或者完全限定名称来导入某个命名空间的。导入和别名的设定是通过use来实现的:

<?php
namespace foo;
use My\Full\Classname as Another;

// 下面的例子与 use My\Full\NSname as NSname 相同
use My\Full\NSname;

// 导入一个全局类
use ArrayObject;

// importing a function (PHP 5.6+)
use function My\Full\functionName;

// aliasing a function (PHP 5.6+)
use function My\Full\functionName as func;

// importing a constant (PHP 5.6+)
use const My\Full\CONSTANT;

obj = new namespace\Another; // 实例化 foo\Another 对象obj = new Another; // 实例化 My\Full\Classname 对象
NSname\subns\func(); // 调用函数 My\Full\NSname\subns\func
$a = new ArrayObject(array(1)); // 实例化 ArrayObject 对象
// 如果不使用 "use \ArrayObject" ,则实例化一个 foo\ArrayObject 对象
func(); // calls function My\Full\functionName
echo CONSTANT; // echoes the value of My\Full\CONSTANT
?>

因为这里规定的use使用的是完全限定名称,所以是不需要加\的。

<?php
use My\Full\Classname as Another, My\Full\NSname;

obj = new Another; // instantiates object of class My\Full\Classnameobj = new \Another; // instantiates object of class Another
obj = new Another\thing; // instantiates object of class My\Full\Classname\thingobj = new \Another\thing; // instantiates object of class Another\thing
?>

如果文件没有定义任何命名空间那么文件里的类和函数都是属于全局命名空间的。 在名称前加上前缀 \表示该名称是全局空间中的名称,即使该名称位于其它的命名空间中时也是如此。

<?php
namespace A\B\C;

/* 这个函数是 A\B\C\fopen */
function fopen() { 
     /* ... */
     f = \fopen(...); // 调用全局的fopen函数
     returnf;
} 
?>

在一个命名空间中,当 PHP 遇到一个非限定的类、函数或常量名称时,它使用不同的优先策略来解析该名称。类名称总是解析到当前命名空间中的名称。因此在访问系统内部或不包含在命名空间中的类名称时,必须使用完全限定名称 。 对于函数和常量来说,如果当前命名空间中不存在该函数或常量,PHP 会退而使用全局空间中的函数或常量。

1.2 自动加载

1.2.1 加载函数

这算是当初比较疑惑的一个问题,文件内没有include或者require,类是怎么引用的呢?

php在开发的过程中可能需要require来引入一个类,但是如果一个文件内需要从外部导入多个类的话,就需要非常多个require或者include,那么整个文件就显得非常臃肿,而且也不能保证是否全部需要的类都被导入了。

针对这个问题,php提供了autoload机制,即所有的类先不导入,等到需要的时候再进行导入操作。这种机制也叫做lazy loading。

类的自动加载函数:__autoload()

通常php在使用一个类的时候若是发现没有导入这个类会自动调用__autoload()函数来导入这个类,这个函数是我们在应用程序中自定义的。

        function __autoload(classname) {
           require_once (classname . "class.php"); 
        }

在这里我们可以看到autoload函数主要需要做3件事:

  1. 根据类名确定类的文件名
  2. 确定类文件所在的文件目录
  3. 将类加载到当前文件中

第三步较为简单,但是第一二步需要约定类名与文件名的映射关系,这样我们才能根据类名来找到响应的文件名,来确定类文件所在的目录。

那么这个函数存在什么样的问题呢?如果一个项目是由多个开发人员开发的,那么他们的类和文件的映射关系可能不尽相同,比如说我的默认根目录是系统目录,A的默认目录是/test目录,这样的映射关系就出现了偏差,对于autoload函数也会出现一些问题,不能很好的映射,因为autoload函数是全局函数只能定义一次,若是把所有的规则都集成到一个函数里面,依旧会显得十分臃肿。这时候就需要采用一个autoload函数的调用栈来解决这个问题,栈内有若干个autoload函数,但是每个函数的对应规则不同,根据不同的规则来调用不同的autoload函数,效率就会高了许多。

其实上面的解决办法和php5引入的spl autoload差不多:

SPL 是 Standard PHP Library (标准 PHP 库) 的缩写。它是 PHP5 引入的一个扩展库,其主要功能包括 autoload 机制的实现及包括各种 Iterator 接口或类。SPL Autoload 具体有几个函数:

spl_autoload_register:注册 _autoload () 函数
spl_autoload_unregister:注销已注册的函数
spl_autoload_functions:返回所有已注册的函数
spl_autoload_call:尝试所有已注册的函数来加载类
spl_autoload :_autoload () 的默认实现
spl_autoload_extionsions: 注册并返回 spl_autoload 函数使用的默认文件扩展名。

spl_autoload就是autoload的默认实现,即去注册的目录中寻找与classname同名的.php/.inc文件。

spl_autoload_registerautoload函数的调用栈,可以通过这个函数注册autoload函数,当php找不到类名的时候会逐个调用调用栈内的autoload函数,实现自动加载的功能。

1.2.2 映射规则

ps0标准:
1、 一个完全合格的 namespace 和 class 必须符合这样的结构:“\< Vendor Name>(< Namespace>)*< Class Name>”
2、每个 namespace 必须有一个顶层的 namespace("Vendor Name" 提供者名字)
3、每个 namespace 可以有多个子 namespace
4、当从文件系统中加载时,每个 namespace 的分隔符 (\) 要转换成 DIRECTORYSEPARATOR (操作系统路径分隔符)
5、在类名中,每个下划线 () 符号要转换成 DIRECTORY_SEPARATOR (操作系统路径分隔符)。在 namespace 中,下划线 _符号是没有(特殊)意义的。
6、当从文件系统中载入时,合格的 namespace 和 class 一定是以 .php 结尾的
7、verdor name,namespaces,class 名可以由大小写字母组合而成(大小写敏感的)

首先每个命名空间都需要有一个顶级的命名空间,顶级空间的名字一般是提供者的名字。

每个顶级的命名空间可以有多个子命名空间。

在文件系统加载文件的时候,每个命名空间的分割符都要被转换成操作系统的路径分隔符,也就是说命名空间是和文件目录息息相关的。下划线也要转换成操作系统路径分隔符。

载入的时候存放类或者命名空间的名字都是以类或者命名空间的名字命名的。

根据以上的几条规则,首先对文件路径做了一定的映射,其次也对文件名做了一定的规范,整体上是规范了文件与命名空间的映射关系的。

有这样的命名空间命名规则和映射标准,我们就可以推理出我们应该把命名空间所在的文件该放在哪里了。依旧以 Symfony\Core\Request 为例, 它的目录是 /path/to/project/vendor/Symfony/Core/Request.php,其中 /path/to/project 是你项目在磁盘的位置,/path/to/project/vendor 是项目用的所有第三方库所在目录。/path/to/project/vendor/Symfony 就是与顶级命名空间 Symfony 存在对应关系的目录,再往下的文件目录就是按照 PSR0 标准建立的:

Symfony\Core\Request => /Symfony/Core/Request.php
ps4标准:

PSR-4和PSR-0最大的区别是对下划线(underscore)的定义不同。PSR-4中,在类名中使用下划线没有任何特殊含义。而PSR-0则规定类名中的下划线_会被转化成目录分隔符。

1.2.3 composer自动加载过程

composer做了哪些事情:

  • 你有一个项目需要使用一些库
  • 你需要的库依赖于其他的库
  • 你声明你需要的库
  • composer会找到指定版本的库,并且安装他

例如,你正在创建一个项目,需要做一些单元测试。你决定使用 phpunit 。为了将它添加到你的项目中,你所需要做的就是在 composer.json 文件里描述项目的依赖关系。

 {
   "require": {
     "phpunit/phpunit":"~6.0",
   }
 }

然后在 composer require 之后我们只要在项目里面直接 use phpunit 的类即可使用。

执行 composer require 时发生了什么:

  • composer 会找到符合 PR4 规范的第三方库的源
  • 将其加载到 vendor 目录下
  • 初始化顶级域名的映射并写入到指定的文件里

(如:'PHPUnit\\Framework\\Assert' => __DIR__ . '/..'.'/phpunit/phpunit/src/Framework/Assert.php'

  • 写好一个 autoload 函数,并且注册到 spl_autoload_register()里

2. 目录结构

app
bootstrap
config 
database
public
resources
routes
storage
tests
vendor

app:

包含应用程序的核心代码, 应用中几乎所有的类都应该放在这里。

bootstrap:

包含引导框架的app.php文件,cache文件夹下存放了框架生成的用来提高性能的文件:缓存。

config:

包含应用程序内所有的配置文件。

database:

包含数据填充和迁移文件以及模板工厂类,作为数据库存放目录。

public:

包含了入口文件,进入应用程序的所有请求的入口点,包含了一些资源文件(图片,css)。

routes:

routes 目录包含了应用的所有路由定义,Laravel 默认包含了几个路由文件:web.php、api.php、 console.php 和 channels.php。

web.php 文件包含 RouteServiceProvider 放置在 web 中间件组中的路由,它提供会话状态、CSRF 防护和 cookie 加密。如果你的应用不提供无状态的、RESTful 风格的 API,则所有的路由都应该在 web.php 文件中定义。

api.php 文件包含 RouteServiceProvider 放置在 api 中间件组中的路由,它提供了频率限制。这些路由都是无状态的,所以通过这些路由进入应用请求旨在通过令牌进行身份认证,并且不能访问会话状态。

console.php 文件是定义所有基于控制台命令闭包函数的地方。每个闭包函数都被绑定到一个命令实例并且允许和命令行 IO 方法进行简单的交互。尽管这些文件没有定义 HTTP 路由,但它也将基于控制台的入口点(路由)定义到应用程序中。

channels.php 用来注册你的应用支持的所有的事件广播渠道的地方。

Storage :
storage 目录包含编译后的 Blade 模板、session 会话生成的文件、缓存文件以及框架生成的其他文件。这个目录被细分成 app 、 framework 和 logs 三个子目录。app 目录可以用来存储应用生成的任何文件。 framework 目录用来存储框架生成的文件和缓存。最后, logs 目录包含应用的日志文件。

Tests :

tests 目录包含自动化测试文件。

Vendor :

vendor 目录包含你所有的 Composer 依赖包。

你的大部分应用程序都位于 app 目录中。默认情况下,此目录的命名空间为 App, 并通过 Composer 使用 PSR-4 自动加载标准自动加载。

app 目录包含额外的各种目录,比如:Console, Http, 和 Providers。将 Console 和 Http 目录视为向应用程序的核心提供 API。HTTP协议和CLI都是与应用程序交互的机制,但实际上并不包含应用程序逻辑。换句话说,它们是向你的应用程序发出命令的两种方式。Console 包含所有的 Artisan 命令,而 Http 目录包含你的控制器,中间件和请求。

当你使用 make Artisan 命令生成类时,将在 app 目录下生成各种其它目录。例如,当你执行make:job Artisan 命令去生成一个作业类时,app/Jobs 目录将自动被创建。

3. 生命周期

https://3rsh1.oss-cn-beijing.aliyuncs.com/20200723184401.png

请求从index.php开始,从index.php结束。

1. require __DIR__.'/../bootstrap/autoload.php';

2. app = require_once __DIR__.'/../bootstrap/app.php';kernel = app->make(Illuminate\Contracts\Http\Kernel::class);

3.response = kernel->handle(request = Illuminate\Http\Request::capture()
   );
   response->send();

4.kernel->terminate(request,response);

上面是index.php的源码。

  1. 文件载入composer生成的自动加载设置,包括所有你 composer require的依赖。其实就是加载初始化第三方依赖。

  2. 生成容器Container,Application应用程序实例,并向容器注册核心组件(HttpKernel,ConsoleKernel ,ExceptionHandler),应该是从app.php获取应用程序实例。

  3. 处理请求,生成并发送响应(对应代码3,毫不夸张的说,你99%的代码都运行在这个小小的handle 方法里面)。 请求被发送到 HTTP 内核或 Console 内核,这取决于进入应用的请求类型。 HTTP 内核的标志性方法 handle处理的逻辑相当简单:获取一个 Request,返回一个 Response,把该内核想象作一个代表整个应用的大黑盒子,输入 HTTP 请求,返回 HTTP 响应。

  4. 请求结束,进行回调(对应代码4,还记得可终止中间件吗?没错,就是在这里回调的)。

第三条细节:

1. 首先 Bootstrap 检测环境,加载 bootstrapper数组中的一些配置

HTTP 内核继承自 Illuminate\Foundation\Http\Kernel 类,该类定义了一个 bootstrappers 数组,这个数组中的类在请求被执行前运行,这些 bootstrappers 配置了错误处理、日志、检测应用环境以及其它在请求被处理前需要执行的任务。

protected $bootstrappers = [
        //注册系统环境配置 (.env)
        'Illuminate\Foundation\Bootstrap\DetectEnvironment',
        //注册系统配置(config)
        'Illuminate\Foundation\Bootstrap\LoadConfiguration',
        //注册日志配置
        'Illuminate\Foundation\Bootstrap\ConfigureLogging',
        //注册异常处理
        'Illuminate\Foundation\Bootstrap\HandleExceptions',
        //注册服务容器的门面,Facade 是个提供从容器访问对象的类。
        'Illuminate\Foundation\Bootstrap\RegisterFacades',
        //注册服务提供者
        'Illuminate\Foundation\Bootstrap\RegisterProviders',
        //注册服务提供者 `boot`
        'Illuminate\Foundation\Bootstrap\BootProviders',
    ];

注意顺序:
Facades 先于ServiceProviders,注册 Facades 就是注册 config\app.php中的aliases 数组,你使用的很多类,如AuthCache,DB等等都是Facades;而ServiceProvidersregister方法永远先于boot方法执行,以免产生boot方法依赖某个实例而该实例还未注册的现象。

Facades 为应用的服务容器提供了一个「静态」 接口。Laravel 自带了很多 Facades,可以访问绝大部分功能。Laravel Facades 实际是服务容器中底层类的 「静态代理」

2. 第一堵墙,全局中间件,默认为 CheckForMaintenanceMode

在Laravel基础的服务启动之后,就要把请求传递给路由了。路由器将会分发请求到路由或控制器,同时运行所有路由指定的中间件。

传递给路由是通过 Pipeline(管道)来传递的,但是Pipeline有一堵墙,在传递给路由之前所有请求都要经过,这堵墙定义在app\Http\Kernel.php中的$middleware数组中,没错就是中间件,默认只有一个CheckForMaintenanceMode中间件,用来检测你的网站是否暂时关闭。这是一个全局中间件,所有请求都要经过,你也可以添加自己的全局中间件。

3. 然后遍历所有注册的路由,找到最先符合的第一个路由

然后遍历所有注册的路由,找到最先符合的第一个路由,

4. 第二堵墙,通过该路由的中间件(组)

经过该路由中间件,进入到控制器或者闭包函数,执行你的具体逻辑代码。

参考链接:

https://learnku.com/articles/4681/analysis-of-the-principle-of-php-automatic-loading-function
http://php.p2hp.com/manual/zh/language.namespaces.rationale.php
https://www.jianshu.com/p/08b810b720d9
https://learnku.com/docs/laravel/7.x

发表评论

textsms
account_circle
email

3rsh1's Blog

lavarel专题1
放假之前就像做个lavarel反序列化漏洞的专题,但是一直在忙考试,没有时间。现在终于可以从头开始搞一下了。 lavarel基础 首先要清楚lavarel是什么, Laravel是一套简洁、优雅的PHP We…
扫描二维码继续阅读
2020-07-23