php详解

如何用PHP计算输出从a到z的字母?(代码详解)

本篇文章主要给大家介绍如何用PHP打印出从“a”到“z”的字母。

在PHP面试过程中,关于用PHP输出1到100或者某个范围的数字,都是比较常见的问题。那么对于PHP学习者来说,应该是比较简单。我们都知道,只要使用基础的PHP循环语句,就可以循环输出我们想要的数据。

但是对于如何输出指定范围内的所有字母,可能对于新手来说,可能有一定的难度,不过也是非常简单的。

下面我们就结合具体的代码示例,给大家介绍如何用PHP打印书序从a到z的所有字母。

具体代码示例如下:

<?php

for ($x = ord('a'); $x <= ord('z'); $x++)

echo chr($x);

echo "\n";

结果如下:

如何用PHP计算输出从a到z的字母?

如图所示,成功打印出a到z的所有字母。是不是非常简单呢?

这里大家只要注意两个重要的函数:

ord()返回字符的 ASCII 码值,即表示返回字符串 string 第一个字符的 ASCII 码值。

chr()返回指定的字符,即返回相对应于 ascii 所指定的单个字符。

注:chr()函数与 ord() 是互补的。

chr和ord函数用法

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是现今最通用的单字节编码系统,并等同于国际标准ISO/IEC 646。

本篇文章就是关于如何用PHP脚本打印出从a到z的所有字母的具体介绍,非常简单易懂。希望对需要的朋友有所帮助!

展开
收起

Thinkphp5开启强制路由代码示例详解(防远程攻击必备)

近期thinkphp5在2018年12月9日及2019年1月11日接连爆出的两个高危漏洞,再有很多人反映更新补丁或者升级以后还有被入侵的事情,分两个步骤处理:一:把受影响的网站程序文件最好直接用备份替换,对方很可能留了多个后门文件,如果后门文件没删除干净,还是会被持续更改首页跳转的,升级也没用;二,开启强制路由模式;

以上的说明,切记一定要保证木马文件查杀赶紧,再做处理,备份直接替换是最好的办法,当然如果你没有备份的习惯就只能自己手动查杀,或者请安全公司处理了;

但是个人在开启强制路由模式的时候,碰到两个问题,下面就说下这个两个问题,

问题1:点击网站链接直接进去会打不开,错误提示,路由未定义

问题2:比如后台的页面或者登陆页面,登陆页面可以正常展示,点击登陆的时候,由于需要post提交表单的操作,错误提示,路由未定义;

强制路由设置:

文件:application->config.php;

配置:常规配置路由规则,请自行参照官方手册,开启强制路由以后,所有的方法都需要配置,否则不能正常访问;这里不赘述;https://www.kancloud.cn/manual/thinkphp5/118019

实例解决方案:

提示:做修改之前一定要备份原有代码,因为每个人的开发环境,开发方式都可能有所不同,若有不可预见错误,可及时恢复;

A:问题1解决方案:

文件:application->route.php;

亲测本人开发环境及网站可用,此问题即可解决;

B:问题2解决方案:

前台及后台纯展示页面在开启强制路由以后展示没问题的,但是需要form表单提交数据的操作都会报错;

理论原因:常规的路由页面方法都是get,比如上图首页的路由,但是提交表单的时候一般我用的都是post方法,所以需要在控制器里面拆分展示和提交表单的方法,前端页面提交方法地址也需要修改,路由里面也需要分开设置即可;下面我将会以一个实例分别说明登录页面及登录操作

登录操作:

第一步:拆分控制器方法;

原始的代码如下:

拆分成两个方法如下:

第二步:路由文件设置:

文件:application->route.php;

二者区别在于方法,登录页面的路由方法是get,操作的话就是post;

第三步:login登录页面form表单的提交的action也需要修改:

☆☆☆小提示,控制器写方法的时候,成功跳转的链接请务必写完整

模块/控制器/方法;其他所有的方法请务必按这种思想修改完成;

文章预告:下个文章将会写一写https的设置须知以及可能有的一些小坑;

展开
收起

PHP开发中经常遇到的Web安全漏洞防御详解

程序员需要掌握基本的web安全知识,防患于未然,你们知道有多少种web安全漏洞吗?这里不妨列举10项吧,你们可以自己去网站找相应的教程来提升自己的1、命令注入(Command Injection)2、eval注入(Eval Injection)3、客户端脚本攻击(Script Insertion)4、跨网站脚本攻击(Cross Site Scripting, XSS)5、SQL注入攻击(SQL injection)6、跨网站请求伪造攻击(Cross Site Request Forgeries, CSRF)7、Session 会话劫持(Session Hijacking)8 Session 会话固定(Sessionfixation)9、HTTP响应拆分攻击(HTTP Response Splitting)10、文件上传漏洞(File Upload Attack)

这篇文章主要给大家介绍最经常遇到的3个:XXS,CSRF, SQL注入。一、XSS漏洞1.XSS简介XSS(Cross Site Scripting),意为跨网站脚本攻击,为了和样式表css(Cascading Style Sheet)区别,缩写为XSS。攻击者主要使用跨站点脚本来读取Cookie或网站用户的其他个人数据。 一旦攻击者获得了这些数据,他就可以假装是该用户登录网站并获得该用户的权限。2.跨站点脚本攻击的一般步骤1,攻击者以某种方式将xss http链接发送给目标用户2.目标用户登录该网站并打开攻击者在登录过程中发送的xss链接。3.网站执行了这个xss攻击脚本4.目标用户页面跳转到攻击者的网站,攻击者获得了目标用户的信息5.攻击者使用目标用户的信息登录网站以完成攻击3. XSS攻击的危险1.窃取用户信息,例如登录帐户,网上银行帐户等。2.使用用户身份读取,篡改,添加,删除公司敏感数据等3.重要商业价值数据的盗窃4.非法转移5.强制电子邮件6.网站挂马7,控制受害者的机器对其他网站发起攻击

4.防止XSS解决方案1)XSS的根本原因是客户端提交的数据未完全过滤,因此重点是过滤用户提交的信息。2)将重要的cookie标记为http only,因此js中的document.cookie语句将不会获取cookie。3)仅允许用户输入我们期望的数据。 例如:年龄 age只允许用户输入数字,数字外的字符被过滤掉。4)对数据进行Html编码处理:当用户提交数据时,将其进行HTML编码,并且在下一次处理之前,将相应的符号转换为实体名称。5)过滤或删除特殊的HTML标签,例如:<script>,<iframe>,<for <,> for>,6)过滤js事件的标签。例如 "onclick=", "onfocus" 等等。二、CSRF攻击(跨站点请求伪造)1.CSRF简介CSRF(Cross Site Request Forgeries),意为跨网站请求伪造,也有写为XSRF。攻击者伪造目标用户的HTTP请求,然后此请求发送到有CSRF漏洞的网站,网站执行此请求后,引发跨站请求伪造攻击。攻击者利用隐蔽的HTTP连接,让目标用户在不注意的情况下单击这个链接,由于是用户自己点击的,而他又是合法用户拥有合法权限,所以目标用户能够在网站内执行特定的HTTP链接,从而达到攻击者的目的。2.CSRF攻击的危害主要危害来自攻击者窃取用户身份并发送恶意请求。 例如:模拟用户的行为,以发送电子邮件,发送消息以及保护诸如付款和转账之类的财产。例如:某个购物网站购买商品时,采用http://www.shop.com/buy.php?item=watch&num=1,item参数确定要购买什么物品,num参数确定要购买数量,如果攻击者以隐藏的方式发送给目标用户链接那么如果目标用户不小心访问以后,购买的数量就成了1000个。3.防止CSRF的解决方案1)POST用于接收重要的数据交互。 当然,POST不是万能药。 锻造形式可能会破裂。2)使用验证码。 只要涉及数据交互,请首先验证验证码。 这种方法可以完全解决CSRF。 但是,出于用户体验的考虑,网站无法向所有操作添加验证码。 因此,验证码只能用作辅助方法,而不能用作主要解决方案。3)验证HTTP Referer字段,该字段记录HTTP请求的源地址。 最常见的应用是图片防盗链。4)为每个表单添加令牌并进行验证。三、SQL注入攻击(SQL injection)1 SQL注入所谓的SQL注入攻击,即当某些程序员编写代码时,他们没有判断用户输入数据的合法性,这使应用程序成为潜在的安全隐患。 用户可以提交一段数据库查询代码,并根据程序返回的结果获取一些他想知道的数据。SQL注入攻击是一种攻击者,他以某种形式提交精心构造的sql语句并更改原始sql语句。 如果Web程序未检查提交的数据,将导致SQL注入攻击。2 SQL注入攻击的一般步骤:1)攻击者访问带有SQL注入漏洞的站点,并寻找注入点2)攻击者构造注入语句,并将注入语句与程序中的SQL语句组合以生成新的SQL语句3)将新的SQL语句提交到数据库进行处理4)数据库执行新的SQL语句,引起SQL注入攻击3 防止SQL注入的方式通常,SQL注入的位置包括:1)表单提交,主要是POST请求,包括GET请求;2)URL参数提交,主要是GET请求参数;3)提交Cookie参数;4)HTTP请求标头中的一些可修改值,例如Referer,User_Agent等;4.举例举一个简单的例子,select * from user where id=100 ,表示查询id为100的用户信息,如果id=100变为 id=100 or 2=2,sql将变为:select * from user where id=100 or 2=2,将把所有user表的信息查询出来,这就是典型的sql注入。5.防止SQL注入的解决方案1)验证用户的输入并使用正则表达式过滤传入的参数2)使用参数化语句,不拼接SQL,也可以使用安全存储过程3)不要对每个应用程序使用具有管理员特权的数据库连接,而对特权限制使用数据库连接4)检查数据存储类型5)重要信息必须加密

以上是本文的全部内容,希望对大家的学习有帮助,也希望大家多多支持 php自学中心 感谢阅读!

展开
收起

「php面试」php魔法函数与魔法变量详解及实例使用教程

php的语法中,有一些系统自带的方法名,均以双下划线开头,它会在特定的情况下被调用。即所谓的魔法函数。

魔术函数

1。__construct()

实例化对象时被调用, 当__construct和以类名为函数名的函数同时存在时,__construct将被调用,另一个不被调用。

2。__destruct() 当删除一个对象或对象操作终止时被调用。

3。__call() 对象调用某个方法, 若方法存在,则直接调用; 若不存在,则会去调用__call函数。

4。__get() 读取一个对象的属性时, 若属性存在,则直接返回属性值; 若不存在,则会调用__get函数。

5。__set() 设置一个对象的属性时, 若属性存在,则直接赋值; 若不存在,则会调用__set函数。

面向对象编程中使用频率很高的两个方法.当设置和获取对象的属性不允许访问时性,此方法会被调用。一定注意是不存在或不允许被读写时才会被调用。

因此对于一个对象,其属性不确定时,用这两个方法效果很好。

__get($name) 获取对象不存在的属性或无法访问的属性时调用.$name表示要获取的属性名

__set($name, $value) 设置对象不存在的属性或无法访问的属性时调用.$name表示要设置的属性名,$value表示要设置的值.

//例如:我们可以构建一个不确定属性的数据记录类

class Record {

protected $_data;

public function __get($name)

{

if (isset($this->_data[$name])) {

return $this->_data;

}

return false;

}

public function __set($name, $value)

{

$this->_data = $value;

}

}

$record = new Record();

$record->name = 'andrew';

echo 'My name is '.$record->name.PHP_EOL;

这两个方法用得比较少些。当调用方法isset()判断对象是否存在某属性, 调用unset()注销某属性时。且当这些属性不存在或不可访问时,会分别调用__isset()和__unset()方法

与前面的__get()和__set()略同。都是某属性不存在或不可访问时被调用

__isset($name) 当调用方法isset()方法判断不可访问的类属性时调用.$name表示属性名.

__unset($name) 当调用方法unset()方法删除不可访问的类属性时调用.$name表示属性名.

6。__toString() 打印一个对象的时被调用。如echo $obj;或print $obj;

classInfo{

publicfunction__toString()

{

return"info";

}

}

$info = new Info();

echo $info.PHP_EOL;

7。__clone() 克隆对象时被调用。如:$t=new Test();$t1=clone $t;

8。__sleep() serialize之前被调用。若对象比较大,想删减一点东东再序列化,可考虑一下此函数。

9。__wakeup() unserialize时被调用,做些对象的初始化工作。

__sleep() 和 __wakeup()

这两个方法,咋一看,就是睡觉和唤醒嘛。那跟对象有什么关系?有时候该用的时候也想不起来。其实,我们简单点记,在php中有一个让对象睡觉的方法,叫searialize(),

它会将对象的各属性序列化以方便保存起来。而unsearialize()方法是将保存的序列化的数据解开变成对象。也叫唤醒。相对应的,当睡觉时,php会调用__sleep()方法,它

的返回值必须是一个数组,表示需要保存的属性项, 对于文件句柄,数据库连接等资源类型的数据是不能被序列化保存的。同理唤醒对象时,php会调用__wakeup()方法,

但与__sleep()不同的是,它返回值为空。被保存的属性都会被解开。那它有什么用呢?刚我们说了,searialize是不能保存资源的。那么唤醒时如果我们想用到这些资源怎么

办?回答很肯定,重新创建?那在哪里创建合适呢?当然是__wakeup()方法里面,因为每次唤醒时都会调用此方法嘛。这下我们很清楚这两个方法的用途了。

__sleep() 当调用searialize()方法时调用,返回值为数组,表示需要序列化的数据项.

__wakeup() 当调用unsearizlie()方法时调用。一般用来在唤醒时初始化资源对象.

//例如我们有一个用户类,用户名和性别都是类属性。用户的密码存在文件中

Class User {

public $username;

public $sex;

public $passFile;

private $_password;

public function __construct($username, $sex, $passFile)

{

$this->username = $username;

$this->sex = $sex;

$this->passFile = $passFile;

$this->_password = file_get_contents($passFile);

}

public function getPassword()

{

return $this->_password;

}

public function __sleep()

{

return array(

'username', 'sex', 'passFile',

);

}

public function __wakeup()

{

$this->_password = file_get_contents($this->passFile);

}

}

$user = new User('andrew', 'male', 'pass.data');

$serializeData = serialize($user);

echo $serializeData.PHP_EOL;

$user = unserialize($serializeData);

echo $user->getPassword().PHP_EOL;

10。__isset() 检测一个对象的属性是否存在时被调用。如:isset($c->name)。

11。__unset() unset一个对象的属性时被调用。如:unset($c->name)。

12。__set_state() 调用var_export时,被调用。用__set_state的返回值做为var_export的返回值。

13。__autoload() 实例化一个对象时,如果对应的类不存在,则该方法被调用。

魔术常量

1。__LINE__ 返回文件中的当前行号。

2。__FILE__ 返回文件的完整路径和文件名。如果用在包含文件中,则返回包含文件名。自 PHP 4.0.2 起,__FILE__ 总是包含一个绝对路径,而在此之前的版本有时会包含一个相对路径。

3。__FUNCTION__ 返回函数名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该函数被定义时的名字(区分大小写)。在 PHP 4 中该值总是小写字母的。

4。__CLASS__ 返回类的名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该类被定义时的名字(区分大小写)。在 PHP 4 中该值总是小写字母的。

5。__METHOD__ 返回类的方法名(PHP 5.0.0 新加)。返回该方法被定义时的名字(区分大小写)。

展开
收起

PHP explode()函数用法详解

本篇文章主要给大家介绍PHP中explode()函数的用法详解,希望对需要的朋友有所帮助!

explode()是PHP中的内置函数,用于将字符串拆分为不同的字符串。explode()函数基于字符串分隔符来拆分字符串,也就是说,它将字符串拆分为出自分隔符的位置。此函数返回一个数组,其中包含通过拆分原始字符串形成的字符串。

简而言之,explode()函数就是用于把字符串打散为数组。

语法说明:

array explode(separator, OriginalString, NoOfElements)

参数:

explode函数接受三个参数,其中两个是强制的,一个是可选的。

separator(分隔符):这个字符指定一个或多个临界点,即,只要在字符串中找到此字符,它就会表示数组的一个元素的结尾和另一个元素的开头。

OriginalString:要在数组中拆分的字符串。

NoOfElements:这是可选的。它用于指定数组的元素数。此参数可以是任何整数(正数,负数或零)

正(N):当此参数以正值传递时,表示该数组将包含此数量的元素。如果分隔符分隔后的元素数量大于这个值,那么前N-1个元素保持不变,最后一个元素是整个剩余的字符串。

负(N):如果负值作为参数传递,那么数组的最后N个元素将被裁剪掉,数组的其余部分将作为单个数组返回。

零:如果此参数为零,则返回的数组将只有一个元素,即整个字符串。

如果未提供此参数,则返回的数组包含使用分隔符分隔字符串后形成的元素总数。

返回类型:

explode()函数的返回类型是字符串数组。

PHP explode()函数代码示例如下:

<?php // 原始字符串 $OriginalString = "Hello, How can we help you?"; // 没有可选参数NoOfElements print_r(explode(" ",$OriginalString)); // 正的NoOfElements print_r(explode(" ",$OriginalString,3)); // 负的NoOfElements print_r(explode(" ",$OriginalString,-1)); ?>

输出:

Array([0] => Hello, [1] => How [2] => can [3] => we [4] => help [5] => you?)Array( [0] => Hello, [1] => How [2] => can we help you?)Array( [0] => Hello, [1] => How [2] => can [3] => we [4] => help)

本文来自PHP中文网,更多相关知识点请前往PHP中文网视频课程频道!

展开
收起

PHP高级之一次请求处理过程或生命周期详解

简介先看看下面这个过程:

我们从未手动开启过PHP的相关进程,它是随着Apache的启动而运行的;PHP通过mod_php5.so模块和Apache相连(具体说来是SAPI,即服务器应用程序编程接口);PHP总共有三个模块:内核、Zend引擎、以及扩展层;PHP内核用来处理请求、文件流、错误处理等相关操作;Zend引擎(ZE)用以将源文件转换成机器语言,然后在虚拟机上运行它;扩展层是一组函数、类库和流,PHP使用它们来执行一些特定的操作。比如,我们需要mysql扩展来连接MySQL数据库;当ZE执行程序时可能会需要连接若干扩展,这时ZE将控制权交给扩展,等处理完特定任务后再返还;最后,ZE将程序运行结果返回给PHP内核,它再将结果传送给SAPI层,最终输出到浏览器上。深入探讨  等等,没有这么简单。以上过程只是个简略版,让我们再深入挖掘一下,看看幕后还发生了些什么。

Apache启动后,PHP解释程序也随之启动;PHP的启动过程有两步;第一步是初始化一些环境变量,这将在整个SAPI生命周期中发生作用;第二步是生成只针对当前请求的一些变量设置。PHP启动第一步  不清楚什么第一第二步是什么?别担心,我们接下来详细讨论一下。让我们先看看第一步,也是最主要的一步。要记住的是,第一步的操作在任何请求到达之前就发生了。

启动Apache后,PHP解释程序也随之启动;PHP调用各个扩展的MINIT方法,从而使这些扩展切换到可用状态。看看php.ini文件里打开了哪些扩展吧;MINIT的意思是“模块初始化”。各个模块都定义了一组函数、类库等用以处理其他请求。  一个典型的MINIT方法如下:PHP_MINIT_FUNCTION(extension_name){/* Initialize functions, classes etc */}PHP启动第二步

当一个页面请求发生时,SAPI层将控制权交给PHP层。于是PHP设置了用于回复本次请求所需的环境变量。同时,它还建立一个变量表,用来存放执行过程中产生的变量名和值。PHP调用各个模块的RINIT方法,即“请求初始化”。一个经典的例子是Session模块的RINIT,如果在php.ini中启用了Session模块,那在调用该模块的RINIT时就会初始化$_SESSION变量,并将相关内容读入;RINIT方法可以看作是一个准备过程,在程序执行之间就会自动启动。  一个典型的RINIT方法如下:PHP_RINIT_FUNCTION(extension_name) {/* Initialize session variables, pre-populate variables, redefine global variables etc */}PHP关闭第一步  如同PHP启动一样,PHP的关闭也分两步:

一旦页面执行完毕(无论是执行到了文件末尾还是用exit或die函数中止),PHP就会启动清理程序。它会按顺序调用各个模块的RSHUTDOWN方法。RSHUTDOWN用以清除程序运行时产生的符号表,也就是对每个变量调用unset函数。  一个典型的RSHUTDOWN方法如下:PHP_RSHUTDOWN_FUNCTION(extension_name) {/* Do memory management, unset all variables used in the last PHP call etc */}PHP关闭第二步  最后,所有的请求都已处理完毕,SAPI也准备关闭了,PHP开始执行第二步:

PHP调用每个扩展的MSHUTDOWN方法,这是各个模块最后一次释放内存的机会。  一个典型的RSHUTDOWN方法如下:PHP_MSHUTDOWN_FUNCTION(extension_name) {/* Free handlers and persistent memory etc */}  这样,整个PHP生命周期就结束了。要注意的是,只有在服务器没有请求的情况下才会执行“启动第一步”和“关闭第二步”。

图1 php结构

从图上可以看出,php从下到上是一个4层体系

①Zend引擎

Zend整体用纯c实现,是php的内核部分,它将php代码翻译(词法、语法解析等一系列编译过程)为可执行opcode的处理并实现相应的处理方法、实现了基本的数据结构(如hashtable、oo)、内存分配及管理、提供了相应的api方法供外部调用,是一切的核心,所有的外围功能均围绕zend实现。

②Extensions

围绕着zend引擎,extensions通过组件式的方式提供各种基础服务,我们常见的各种内置函数(如array系列)、标准库等都是通过extension来实现,用户也可以根据需要实现自己的extension以达到功能扩展、性能优化等目的(如贴吧正在使用的php中间层、富文本解析就是extension的典型应用)。

③Sapi

Sapi全称是Server Application Programming Interface,也就是服务端应用编程接口,sapi通过一系列钩子函数,使得php可以和外围交互数据,这是php非常优雅和成功的一个设计,通过sapi成功的将php本身和上层应用解耦隔离,php可以不再考虑如何针对不同应用进行兼容,而应用本身也可以针对自己的特点实现不同的处理方式。后面将在sapi章节中介绍

④上层应用

这就是我们平时编写的php程序,通过不同的sapi方式得到各种各样的应用模式,如通过webserver实现web应用、在命令行下以脚本方式运行等等。

构架思想:

引擎(Zend)+组件(ext)的模式降低内部耦合

中间层(sapi)隔绝web server和php

**************************************************************************

如果php是一辆车,那么

车的框架就是php本身

Zend是车的引擎(发动机)

Ext下面的各种组件就是车的轮子

Sapi可以看做是公路,车可以跑在不同类型的公路上

而一次php程序的执行就是汽车跑在公路上。

因此,我们需要:性能优异的引擎+合适的车轮+正确的跑道

把php最终集成到Apache系统中,还需要对Apache进行一些必要的设置。这里,我们就以php的mod_php5 SAPI运行模式为例进行讲解,至于SAPI这个概念后面我们还会详细讲解。

假定我们安装的版本是Apache2 和 Php5,那么需要编辑Apache的主配置文件http.conf,在其中加入下面的几行内容:

Unix/Linux环境下:

LoadModule php5_module modules/mod_php5.so

AddType application/x-httpd-php .php

注:其中modules/mod_php5.so 是X系统环境下mod_php5.so文件的安装位置。

Windows环境下:

LoadModule php5_module d:/php/php5apache2.dll

AddType application/x-httpd-php .php

注:其中d:/php/php5apache2.dll 是在Windows环境下php5apache2.dll文件的安装位置。

这两项配置就是告诉Apache Server,以后收到的Url用户请求,凡是以php作为后缀,就需要调用php5_module模块(mod_php5.so/ php5apache2.dll)进行处理。

Apache请求处理循环详解 Apache请求处理循环的11个阶段都做了哪些事情呢?

1、Post-Read-Request阶段

在正常请求处理流程中,这是模块可以插入钩子的第一个阶段。对于那些想很早进入处理请求的模块来说,这个阶段可以被利用。

2、URI Translation阶段 Apache在本阶段的主要工作:将请求的URL映射到本地文件系统。模块可以在这阶段插入钩子,执行自己的映射逻辑。mod_alias就是利用这个阶段工作的。

3、Header Parsing阶段 Apache在本阶段的主要工作:检查请求的头部。由于模块可以在请求处理流程的任何一个点上执行检查请求头部的任务,因此这个钩子很少被使用。mod_setenvif就是利用这个阶段工作的。

4、Access Control阶段 Apache在本阶段的主要工作:根据配置文件检查是否允许访问请求的资源。Apache的标准逻辑实现了允许和拒绝指令。mod_authz_host就是利用这个阶段工作的。

5、Authentication阶段 Apache在本阶段的主要工作:按照配置文件设定的策略对用户进行认证,并设定用户名区域。模块可以在这阶段插入钩子,实现一个认证方法。

6、Authorization阶段 Apache在本阶段的主要工作:根据配置文件检查是否允许认证过的用户执行请求的操作。模块可以在这阶段插入钩子,实现一个用户权限管理的方法。

7、MIME Type Checking阶段 Apache在本阶段的主要工作:根据请求资源的MIME类型的相关规则,判定将要使用的内容处理函数。标准模块mod_negotiation和mod_mime实现了这个钩子。

8、FixUp阶段 这是一个通用的阶段,允许模块在内容生成器之前,运行任何必要的处理流程。和Post_Read_Request类似,这是一个能够捕获任何信息的钩子,也是最常使用的钩子。

9、Response阶段 Apache在本阶段的主要工作:生成返回客户端的内容,负责给客户端发送一个恰当的回复。这个阶段是整个处理流程的核心部分。

10、Logging阶段 Apache在本阶段的主要工作:在回复已经发送给客户端之后记录事务。模块可能修改或者替换Apache的标准日志记录。

11、CleanUp阶段 Apache在本阶段的主要工作:清理本次请求事务处理完成之后遗留的环境,比如文件、目录的处理或者Socket的关闭等等,这是Apache一次请求处理的最后一个阶段。

展开
收起

PHP substr()函数的用法详解

本篇文章主要给大家介绍PHP stubstr()函数的用法,substr()是PHP中的内置函数,用于提取字符串的一部分,即返回字符串的子串。

语法:

substr(string_name, start_position, string_length_to_cut)

参数:

substr()函数允许三个参数,其中两个是强制的,一个是可选的。

string_name:在这个参数中,我们传递原始字符串或需要剪切或修改的字符串。这是一个强制参数。

start_position:如果 start_position是非负数,返回的字符串将从 string 的 start 位置开始,从 0 开始计算。如果 start_position是负数,返回的字符串将从 string 结尾处向前数第 start 个字符开始。这也是一个强制参数。

string_length_to_cut:此参数是可选的,为整数类型。这指的是需要从原始字符串中剪切的字符串部分的长度。如果整数是正数,则它指的是从start_position开始并从头开始提取长度。如果整数是负数,那么它指的是从start_position开始并从字符串的结尾提取长度。如果未传递此参数,则substr()函数将返回从start_position开始直到字符串结尾的字符串。

返回类型:

如果成功则返回提取的字符串部分;如果失败,返回FALSE或空字符串。

PHP substr()函数的用法示例:

<?phpfunction Substring($str){$len = strlen($str); echo substr($str, 6), "<br>"; echo substr($str, 3, $len), "<br>"; echo substr($str, -11, 11), "<br>"; echo substr($str,-11, -8), "<br>";}$str="phpAndmysql";Substring($str);

输出:

mysqlAndmysqlphpAndmysqlphp

本文来自PHP中文网,更多相关知识点请前往PHP中文网视频课程频道!

展开
收起

详解 PHP 7.4 的类型属性

PHP 7.4 中增加了类型化类属性,对 php 的类型系统进行了重大改进。这些更改完全是自愿加入的,不会破坏以前的版本。

在本文中,我们将深入了解该功能,但首先让我们总结一下最重要的几点:

这些更改自 PHP 7.4 起可用,计划于 2019 年 11 月发布它们仅在类中可用,并且需要访问修饰符:public、protected 或 private;或 var允许所有类型,但 void 和 callable 除外他们的实际情况是这样的:

classFoo{public int $a;public?string $b='foo';private Foo $prop;protectedstatic string $static='default';}如果你不确定类型的额外好处,我建议您首先阅读这篇文章。

未初始化

在进入正题之前,首先要探讨一个与类型属性有关的重要方面。

不管你第一眼看到这段代码是怎么想的,但它的确是合法的

classFoo{public int $bar;}$foo=newFoo;即便是类的实例化后 $bar 值仍不是整数值的情况下,PHP 也只是会在访问 $bar 时才会报错:

var_dump($foo->bar);Fatal error: Uncaught Error: Typed property Foo::$barmust not be accessed before initialization从错误消息中可以看出,出现了一种新的变量状态:未初始化 (uninitialized)

$bar 属性无论是否声明了类型,值都可以为 null。因此,无法确定类型属性是否设置。这就是增加变量「未初始化」状态的原因。

未初始化有四个方面需要注意:

无法读取未初始化的属性,一旦这么做,将引发致命错误;由于在访问属性时会检查未初始化状态,所以即使是不可为空的对象也可以使用未初始化属性;在读取未初始化属性时候之前可以对其进行写入;unset 操作会让类型属性变成未初始化状态,而非类型属性只会变成值为 null;特别要注意在对象实例化之后设置未初始化的类型属性是合法的:

classFoo{public int $a;}$foo=newFoo;$foo->a=1;// 合法$foo->a=null;// 非法虽然只会在读取属性值时检查未初始化状态,但在写入属性时会进行类型验证。这意味着任何无效的属性值都不会被设置。

默认值和构造函数

让我们仔细看看如何初始化类型属性值。对于标量类型,可以直接提供一个默认值

classFoo{public int $bar=4;public?string $baz=null;// 错误写法 public string $baz = null;publicarray$list=[1,2,3];}类型属性不能显示设置为 null,除非是可空类型。这看上去显而易见的,但是一些旧行为却允许这种操作

functionpassNull(int $i=null){/* … */}passNull(null);幸运的是,类型属性不允许这种令人疑惑的行为。还要注意,属性类型的默认值不可能为对象或者类,你应当使用构造器来设置这些值。

最明显的用来设置默认值的地方就是构造函数

classFoo{private int $a;publicfunction__construct(int $a){$this->a=$a;}}但也要记住我之前提到的内容:在构造函数之外写入未初始化 (uninitialized) 的属性是有效的。只要没有读取属性值的操作,编译器就不会执行未初始化的相关检查。

类型

那么到底哪些类型可以指定,又如何指定呢?我已经提到了指定属性类型只能在类中进行 (当前如此),并且它们需要一个访问修饰符或是属性前面的 var 关键字。

对于可用类型,几乎所有类型都可以使用,除了 void 和 callable 类型.

因为 void 意味着没有值,所以它不能用于指定一个值的类型也就说得过去了。然而 callback 就有一点细微不同了。

可见,PHP 中的 "callback" 可以这样写

$callable=[$this,'method'];假设你有以下 (无效) 代码:

classFoo{publiccallable$callable;publicfunction__construct(callable$callable){/* … */}}classBar{public Foo $foo;publicfunction__construct(){$this->foo=newFoo([$this,'method'])}privatefunctionmethod(){/* … */}}$bar=newBar;($bar->foo->callable)();在此例中,$callback 引用了私有的 Bar::method,但是是在 Foo 的上下文中被调用的。基于这个问题,决定不添加 callback 类型的支持。

不过这并不是什么大问题,因为 Closure(闭包) 是一种有效类型,它会记住构建它的 $this 上下文。

顺带一说,以下是所有可用类型的列表:

boolintfloatstringarrayiterableobject? (nullable)self & parentClasses & interfaces强制和严格类型

PHP,是我们既喜欢又反感的动态语言,它会尽可能地强制或转换类型。假设你在一个期望接受 int 的地方传入字符串,PHP 会试着自动转换该字符串:

functioncoerce(int $i){/* … */}coerce('1');// 1同样的原则也适用于已指定类型的属性,下面的代码是有效的,且会将'1' 转换为 1.

classBar{public int $i;}$bar=newBar;$bar->i='1';// 1如果你并不喜欢这种 (自动转换) 行为,可以通过声明严格类型来禁用它:

declare(strict_types=1);$bar=newBar;$bar->i='1';// 1Fatal error: Uncaught TypeError:Typed property Bar::$i must be int, string used类型差异和继承

即使 PHP 7.4 引入了 改进的类型差异 , 但是类型的属性仍然是不变的。这意味着以下写法是无效的:

classA{}classBextendsA{}classFoo{publicA$prop;}classBarextendsFoo{publicB$prop;}Fatal error: Type of Bar::$prop must be A(as in classFoo)如果上面的示例看起来不够明显的话,你可以查看以下内容:

classFoo{public self $prop;}classBarextendsFoo{public self $prop;}在运行代码之前,PHP 将在背后用它所引用的具体实现类来替换 self。这意味着在此本例中将抛出相同的错误。解决此问题的唯一方法是执行以下操作:

classFoo{public Foo $prop;}classBarextendsFoo{public Foo $prop;}谈到继承,您可能会发现很难想出任何好的用例来重写继承属性的类型。

尽管我同意这种观点,但值得注意的是更改继承属性的类型是可能实现的,前提是访问修饰符也必须从 private 更改为 protected 或 public。

以下代码是有效的:

classFoo{private int $prop;}classBarextendsFoo{public string $prop;}但是,从可空的类型改为不可空或反向的类型是不允许的。

classFoo{public int $a;public?int $b;}classBarextendsFoo{public?int $a;public int $b;}Fatal error: Type of Bar::$a must be int (as in classFoo)还有更多!

正如开头所说,类型化属性是 PHP 的 主要 补充。关于它们更多的内容,我建议您通读 RFC 以了解所有细节。

如果您不熟悉 PHP 7.4,则可能需要阅读 完整列表 中所做的更改和添加的功能。老实说,这是很长一段时间以来最好的发行版之一,值得您花时间!

展开
收起

黑马程序员:PHP程序员必看—详解PHP加密

数据加密在我们生活中的地位已经越来越重要了,尤其是考虑到在网络上发生的大量 交易和传输的大量数据。如果对于采用安全措施有兴趣的话,也一定会有兴趣了解PHP提供的一系列安全功能。在本篇文章中,我们将介绍这些功能,提供一些基本的用法,以便你能够为自己的应用软件中增加安全功能。

预备知识

在详细介绍PHP的安全功能之前,我们需要花点时间来向没有接触过这方面内容的读者介绍一些有关密码学的基本知识,如果对密码学的基本概念已经非常熟悉,就可以跳 过去这一部分。 密码学可以通俗地被描述为对加/解密的研究和实验,加密是将易懂的资料转换为不易懂资料的过程,解密 则是将不易懂的资料转换为原来易懂资料的过程。不易懂的资料被称作密码,易懂的资料被称作明码。 数据的加/解密都需 要一定的算法,这些算法可以非常地简单,如著名的凯撒码,但当前的加密算法要相对复杂得多,其中一些利用现有的方法甚至是无法破译的 。

PHP的加密功能只要有一点使用非Windows平台经验的人可能对crypt()也相当熟悉,这一函数完成被称作单向加密 的功能,它可以加密一些明码,但不能够将密码转换为原来的明码。尽管从表面上来看这似乎是一个没有什么用处的功能,但它的确被广泛用 来保证系统密码的完整性。因为,单向加密的口令一旦落入第三方人的手里,由于不能被还原为明文,因此也没有什么大用处。在验证用户输 入的口令时,用户的输入采用的也是单向算法,如果输入与存储的经加密后的口令相匹配,则输入的口信一定是正确的。PHP同样提供了使用其crypt()函数完成单向加密功能的可能性。我将在这里简要地介绍该函数:

string crypt (string input_string [, string salt])

其中的input_string参数是需要加密的字符串,第二个可选的salt是一个位字串,它能够影响 加密的暗码,进一步地排除被称作预计算攻击的可能性。缺省情况下,PHP使用一个2个字符的DES干扰串,如果你的系统使用的是MD5(我将在 以后介绍MD5算法),它会使用一个12个字符的干扰串。顺便说一下,可以通过执行下面的命令发现系统将要使用的干扰串的长度:

print "My system salt size is: ". CRYPT_SALT_LENGTH;

系统也可能支持其他的加密算法。crypt()支持四 种算法,下面是它支持的算法和相应的salt参数的长度:

算法 Salt长度 CRYPT_STD_DES 2-character (Default) CRYPT_EXT_DES 9-character CRYPT_MD5 12-character beginning with $ CRYPT_BLOWFISH 16-character beginning with $ 用crypt()实现用户身份验证 作为crypt()函数的一个例子,考虑这样一种情况,你希望创建一段PHP脚本程序限 制对一个目录的访问,只允许能够提供正确的用户名和口令的用户访问这一目录。我将把资料存储在我喜欢的数据库MySQL的一个表中。下面我 们以创建这个被称作members的表开始我们的例子: mysql>CREATE TABLE members ( ->username CHAR(14) NOT NULL, ->password CHAR(32) NOT NULL, ->PRIMARY KEY(username) ->);

mysql>CREATE TABLE members ( ->username CHAR(14) NOT NULL, ->password CHAR(32) NOT NULL, ->PRIMARY KEY(username) ->);

然后,我们假定下面的数据已经存储在该表中: 用户名 密码 clark keloD1C377lKE bruce ba1T7vnz9AWgk peter paLUvRWsRLZ4U 这些加密的口令对应的明码分别是kent、banner和parker。注意一下每个口令的前二个字母, 这是因为我使用了下面的代码,根据口令的前二个字母创建干扰串的:

$enteredPassword. $salt = substr($enteredPassword, 0, 2); $userPswd = crypt($enteredPassword, $salt); // $userPswd然后就和用户名一起存储在MySQL 中

我将使用Apache的口令-应答认证配置提示用户输入用户名和口令,一个鲜为人知的有关PHP的信息是,它可以把Apache 的口令-应答系统输入的用户名和口令识别为$PHP_AUTH_USER和$PHP_AUTH_PW,我将在身份验证脚本中用到这二个变量。花一些时间仔细阅读下 面的脚本,多注意一下其中的解释,以便更好地理解下面的代码: crypt()和Apache的口令-应答验证系统的应用

$host = "localhost"; $user = "zorro"; $pswd = "hell odolly"; $db = "users"; // Set authorization to False $authorization = 0; // Verify that user has entered username and password if (isset($PHP_AUTH_USER) &&isset($PHP_AUTH_PW)) : mysql_pconnect($host, $user, $pswd) or die("Can't connect to MySQL server!"); mysql_select_db($db) or die("Can't select database!"); // Perform the encryption $salt = substr($PHP_AUTH_PW, 0, 2); $encrypted_pswd = crypt($PHP_AUTH_PW, $salt); // Build the query $query = "SELECT username FROM members WHERE username = '$PHP_AUTH_USER' AND password = '$encrypted_pswd'"; // Execute the query if (mysql_numrows(mysql_query($query)) == 1) : $authorization = 1; endif; endif; // confirm authorization if (! $authorization) : header('WWW-Authenticate: Basic realm="Private"'); header('HTTP/1.0 401 Unauthorized'); print "You are unauthorized to enter this area."; exit; else : print "This is the secret data!"; endif; >

上面就是一个核实用户访问权限的简单身份验证系统。在使用crypt()保护重要的机密资料时,记住在缺省状态下使用的 crypt()并不是最安全的,只能用在对安全性要求较低的系统中,如果需要较高的安全性能,就需要我在本篇文章的后面介绍的算法。 下面我将介绍另一个PHP支持的函数━━md5(),这一函数使用MD5散列算法,它有几种很有趣的用法值得一提:

混编一个混编函数可以将一个可变长度的信息变换为具有固定长度被混编过的输出,也被称作“信息文摘”。这是十分有用的,因为 一个固定长度的字符串可以用来检查文件的完整性和验证数字签名以及用户身份验证。由于它适合于PHP,PHP内置的md5()混编函数将把一个可 变长度的信息转换为128位(32个字符)的信息文摘。混编的一个有趣的特点是不能通过分析混编后的信息得到原来的明码,因为混编后的结果 与原来的明码内容没有依赖关系。 即便只改变一个字符串中的一个字符,也将使得MD5混编算法计算出二个截然不同的结果。我们首先来看下 表的内容及其相应的结果: 使用md5()混编字符串

$msg = "This is some message that I just wrote"; $enc_msg = md5($msg); print "hash: $enc_msg ";

结果: hash: 81ea092649ca32b5ba375e81d8f4972c 注意,结果的长度为32个字符。再来看一下下面的表,其中的$msg的值有了一点 微小的变化: 使用md5()对一个稍微变化的字符串进行混编

//注意,message中少了一个s $msg = "This is some mesage that I just wrote"; $enc_msg = md5($msg); print "hash2: $enc_msg ";

结果: hash2: e86cf511bd5490d46d5cd61738c82c0c 可以 发现,尽管二个结果的长度都是32个字符,但明文中一点微小的变化使得结果发生了很大的变化,因此,混编和md5()函数是检查数据中微小变 化的一个很好的工具。 尽管crypt()和md5()各有用处,但二者在功能上都受到一定的限制。在下面的部分中,我们将介绍 二个非常有用的被称作Mcrypt和Mhash的PHP扩展,将大大拓展PHP用户在加密方面的选择。 尽管我们在上面的小节中说明了 单向加密的重要性,但有时我们可能需要在加密后,再把密码数据还原成原来的数据,幸运的是,PHP通过Mcrypt扩展库的形式提供了这种可能 性。 Mcrypt Mcrypt 2.5.7 Unix | Win32 Mcrypt 2.4.7是一个功能强大的加密算法扩展库,它包括有22种算法 ,其中就包括下面的几种算法: Blowfish RC2 Safer-sk64 xtea Cast-256 RC4 Safer-sk128 DES RC4-iv Serpent Enigma Rijndael-128 Threeway Gost Rijndael-192 TripleDES LOKI97 Rijndael-256 Twofish PanamaSaferplus Wake

安装: 在标准的PHP软件包中不包括Mcrypt,因此需要下载它,下载后,按照下面的方法进行编译,并把它扩充在PHP中:

gunzipmcrypt-x.x.x.tar.gz tar -xvfmcrypt-x.x.x.tar./configure --disable-posix-threads makemake installcd to your PHP directory. ./configure -with-mcrypt=[dir] [--other-configuration-directives] makemake install

当然了,根据你的 要求和PHP安装时与互联网服务器软件的关系,上面的过程可能需要作适当的修改。

使用Mcrypt Mcrypt的优点不仅仅 在于其提供的加密算法较多,还在于它可以对数据进行加/解密处理,此外,它还提供了35种处理数据用的函数。尽管对这些函数进行详细介绍 已经超出了这篇文章的范围,我还是要就几个典型的函数作一下简要的介绍。 首先,我将介绍如何使用Mcrypt扩展库对数 据进行加密,然后再介绍如何使用它进行解密。下面的代码对这一过程进行了演示,首先是对数据进行加密,然后在浏览器上显示加密后的数 据,并将加密后的数据还原为原来的字符串,将它显示在浏览器上。 使用Mcrypt对数据进行加、解密

// Designate string to be encrypted $string = "Applied Cryptography, by Bruce Schneier, is a wonderful cryptography reference."; // Encryption/decryption key $key = "Four score and twenty years ago"; // Encryption Algorithm $cipher_alg = MCRYPT_RIJNDAEL_128; // Create the initialization vector for added security. $iv = mcrypt_create_iv(mcrypt_get_iv_size($cipher_alg, MCRYPT_MODE_ECB), MCRYPT_RAND); // Output original string print "Original string: $string"; // Encrypt $string $encrypted_string = mcrypt_encrypt($cipher_alg, $key, $string, MCRYPT_MODE_CBC, $iv); // Convert to hexadecimal and output to browser print "Encrypted string: ".bin2hex($encrypted_string).""; $decrypted_string = mcrypt_decrypt($cipher_alg, $key, $encrypted_string, MCRYPT_MODE_CBC, $iv); print "Decrypted string: $decrypted_string";

执行上面的脚本将会产生下面的输出: Original string: Applied Cryptography, by Bruce Schneier, is a wonderful cryptography reference. Encrypted string: 02a7c58b1ebd22a9523468694b091e60411cc4dea8652bb8072 34fa06bbfb20e71ecf525f29df58e28f3d9bf541f7ebcecf62b c89fde4d8e7ba1e6cc9ea24850478c11742f5cfa1d23fe22fe8 bfbab5e Decrypted string: Applied Cryptography, by Bruce Schneier, is a wonderful cryptography reference. 上面的代码中二个最典型的函数是mcrypt_encrypt()和 mcrypt_decrypt(),它们的用途是显而易见的。我使用了“电报密码本”模式,Mcrypt提供了几种加密方式,由于每种加密方式都有可以影响 密码安全的特定字符,因此每种模式都需要了解。对于没有接触过密码系统的读者来说,可能对mcrypt_create_iv()函数更有兴趣,尽管对这 一函数进行彻底的解释已经超出了本篇文章的范围,但我仍然会提到它创建的初始化向量(hence, iv),这一向量可以使每条信息彼此独立。 尽管不是所有的模式都需要这一初始化变量,但如果在要求的模式中没有提供这一变量,PHP就会给出警告信息。

Mhash扩展库 0.8.3版的Mhash扩展库支持12种混编算法,仔细检查Mhash v.0.8.3的头文件mhash.h可以知道,它支持下面的混编算法: CRC32 HAVAL160 MD5 CRC32B HAVAL192 RIPEMD160 GOST HAVAL224 SHA1 HAVAL128 HAVAL256 TIGER

安装 象Mcrypt一 样,Mhash也没有包括在PHP软件包中,对于非Windows用户而言,下面是安装过程: 下载Mhash扩展库

gunzipmhash-x.x.x.tar.gz tar -xvfmhash-x.x.x.tar./configuremakemake installcd./configure -with-mhash=[dir] [--other-configuration-directives] makemake install

象Mcrypt一样 ,根据PHP在互联网服务器软件上的安装方式,可能需要对Mhash进行其他的配置。

使用Mhash 对信息进行混编非常简单,看一下下面的例子:

$hash_alg = MHASH_TIGER; $message = "These are the directions to the secret fort. Two steps left, three steps right, and cha chacha."; $hashed_message = mhash($hash_alg, $message); print "The hashed message is ". bin2hex($hashed_message);

执行这一段脚本程序将得到下面的输出结果:

The hashed message is 07a92a4db3a4177f19ec9034ae5400eb60d1a9fbb4ade461

在这里使用bin2hex()函数的目的是方便我们理解$hashed_message 的输出,这是因为混编的结果是二进制格式,为了能够将它转化为易于理解的格式,必须将它转换为十六进制格式。 需要 注意的是,混编是单向功能,其结果不依赖输入,因此可以公开显示这一信息。这一策略通常用于让用户比较下载文件和系统管理员提供的文 件,以确保文件的完整性。 Mhash还有其他一些有用的函数。例如,我需要输出一个Mhash支持的算法的名字,由于 Mhash支持的所有算法的名字都以MHASH_开头,因此,可以通过执行如下的代码完成这一任务:

$hash_alg = MHASH_TIGER; print "This data has been hashed with the".mhash_get_hash_name($hashed_message)."hashing algorithm.";

得到的输出是: This data has been hashed with the TIGER hashing algorithm. 关于PHP和加密最后需要注意的一个问题 关于PHP和加密需要注意的最后的一个重要问题是在服务器和客户端之间传输的数据 在传输过程中是不安全的!PHP是一种服务器端技术,不能阻止数据在传输过程中泄密。因此,如果想实现一个完整的安全应用,建议选用 Apache-SSL或其他的安全服务器布置。

结论这篇文章介绍了PHP最有用的功能之一━━数据加密,不仅讨论了PHP内置 的crypt() 和md5()加密函数,还讨论了用于数据加密的功能强大的扩展库━━Mcrypt和Mhash。在这篇文章最后,我需要指出的是,一个真正安全的PHP应用还应该包括安全的服务器,由于PHP是一种服务器端的技术,因此,在数据由客户端向服务器端进行传输时,它不能保证数据的安全。

展开
收起

PHP文件操作详解

一、打开关闭文件

1、fopen()函数打开文件,它有两个参数第一个是文件名,第二个是打开方式。

// 获取文件路径 $filePath = "psg.txt"; // 打开文件,将资源绑定到一个流或者句柄,绑定之后,脚本就可以通过句柄与此资源交互。 $fileHandle = fopen($filePath, "a+");

fopen()函数第二个参数可能的值

mode

说明

r只读方式打开,文件从头开始读。

r+读写方式打开,文件从头开始读写。

w只写方式打开文件,从文件开头开始写。如果文件已经存在,将文件指针指向文件头并将文件大小截为零,即删除所有文件已有的内容,如果文件不存在,函数将创建这个文件。

w+读写方式打开文件,如果文件已经存在,将文件指针指向文件头并将文件大小截为零。即删除所有文件已有的内容,如果文件不存在,函数将创建这个文件。

a写入方式打开,将文件指针指向文件末尾。如果该文件已有内容,将从文件末尾开始追加,如果该文件不存在,函数将创建这个文件。

a+读写方式打开,将文件指针指向文件末尾。如果该文件已有内容,将从文件末尾开始追加或者读,如果该文件不存在,函数将创建这个文件。

作者:XZ阳光小熊链接:https://www.jianshu.com/p/e845ab9e85c6來源:简书简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

展开
收起