写给女朋友:理解服务端的工作原理(下) April 5, 2018 [TOC] 下面真的进入正题了,上一篇扯了一些杂七杂八应该知道的东西,鉴于篇幅考虑,我还是分开吧。本文主要介绍`拍黄片`,主要演示`Cookie`,`Session`和`Token`鉴权。 这一部分最好可以动手做一下,学点PHP,掌握一门服务端语言。 ### 环境搭建 下载`phpStudy`最新版,安装或者解压,以非服务模式运行,如图: ![](/images/2018/04/3926581271.png) 此时访问`http://127.0.0.1/phpinfo.php`,如果出现页面显示一些关于PHP版本等的信息,环境就算OK了。 注意到右下角`其他选项菜单`没,里面有`网站根目录`: ![](/images/2018/04/2013534176.png) 打开根目录后,你能看到里面有个`phpinfo.php`,你刚才访问的那个页面就是有这个文件渲染出来的。 在这个目录新建一个`hi.php`,输入以下内容: ``` ``` 你该怎么从浏览器访问这个文件呢? OK,如果你访问到了,证明你已经明白了PHP的`路由`机制,所谓路由,你可以简单粗暴的理解为你输入的`URL`和你服务端代码里的那个`函数`或者`文件`相对应。原生PHP(没用框架),路由规则为`域名/目录1/目录2/文件名`对应你网站根目录下`网站根目录/目录1/目录2/文件吗`。话说回来,这个还不算真正意义上的路由,真正一样上的路由为你访问`/page/1`,对应到`/page.php?num=1`,这样一个URL和你代码的对应关系为路由,一般框架都有自己的路由机制。不明白的话就先别管路由了,目前知道原生PHP怎么访问就好啦。 访问结果: ![](/images/2018/04/2397092342.png) ### PHP 入门 - 脚本语言,可以与HTML混编 - 代码写在 之间 - 变量名都是以`$`开头,命名规则类似C - 弱类型语言 - 控制语句基本同C - 注释方式和C一样 恭喜,你入门了PHP初级编程,面向过程的编程方法。之后实验什么地方看不动就去菜鸟教程或者W3SCHOOL查一下就好啦。不要怕,下面实验都有注释的。 ### 与HTML混编 咱们先新建一个目录吧,在网站根目录下新建一个`v1`文件夹,在该文件夹中新建一个`index.php`,内容如下: ```php 用户信息 姓名: 年纪: 性别: ``` 所谓混编呢,就是HTML可以和PHP代码混在一起写,但是注意,PHP代码必须在里面包着。 怎么访问这个页面呢?浏览器地址栏输入`http://127.0.0.1/v1/`是不是也可以?`index.php`一般为服务器默认加载的文件,所以可以省略,这里`http://127.0.0.1/v1/`等价于`http://127.0.0.1/v1/index.php`。 ### 获取 GET 请求内容 PHP里面有个预定义变量,叫`$_GET`,是个数组,用来保存客户端的GET请求内容。还是在`index.php`里面,修改5~7行: ```php ``` 分别访问`http://127.0.0.1/v1/`,`http://127.0.0.1/v1/?name=张三`,`http://127.0.0.1/v1/?name=张三&age=20&sex=男`。结合上一篇文章对GET的介绍理解以下。这里通过这个预定义数组,获得了用户的GET请求的数据。 什么时候用GET呢?比如我博客里有很多篇文章,每次发文章就新增到数据库里,每篇文章有一个id,也是递增的。我可以写一个`detail.php`,通过GET传入`id`参数来输出不同的页面。比如`detail.php?id=1`输出了第一篇文章,`detail.php?id=2`输出第二篇文章。但是你会发现很多时候访问可能是`detail/1.html`,`detail/2.html`,这是为了对搜索引擎的爬虫爬取优化做了`伪静态`,通过`路由`功能实现。 ### 获取 POST 请求内容 同理,`$_POST`预定义数组存储了POST请求内容,新建`login.php`: ```php 用户名: 密 码: 登录 ``` 访问提交数据试试,提交数据的时候,F12看一下提交的请求内容: ![](/images/2018/04/2980953790.png) 这一步叫`抓包`,一般呢,抓包还有其他单独的工具,比如`Fiddler`,为了更深入的理解HTTP协议,我用这个工具抓一次包,然后显示出`Raw`原始内容来看看: ![](/images/2018/04/1528466858.png) 上面是客户端请求的内容,下面是服务端返回的内容。我复制过来,你可以仔细看看里面都有什么,比如,上面请求的时候包含了cookie,下面响应内容里包含了服务端版本信息,系统信息,页面编码信息等。 请求: ``` POST http://127.0.0.1/v1/login.php HTTP/1.1 Host: 127.0.0.1 Connection: keep-alive Content-Length: 27 Cache-Control: max-age=0 Origin: http://127.0.0.1 Upgrade-Insecure-Requests: 1 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Referer: http://127.0.0.1/v1/login.php Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: __uvt=; uvts=6QR1rsXDBappbiwN username=root&password=root ``` 这里的POST格式是**正宗**的,与请求头相隔一行。 响应: ``` HTTP/1.1 200 OK Date: Thu, 05 Apr 2018 12:49:18 GMT Server: Apache/2.4.23 (Win32) OpenSSL/1.0.2j mod_fcgid/2.3.9 X-Powered-By: PHP/7.1.10 Keep-Alive: timeout=5, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html; charset=UTF-8 225 用户名:root 密 码:root 登录 0 ``` 什么时候用POST?登录,提交表单,上传图片等。 **HTTP协议除了GET/POST还有其他的请求方法,PUT/DELETE等** ### 什么是 Cookie 我通俗的这样解释:Cookie是存储在浏览器端的小量信息,是持久化存储的,可以设定存储时间,每次浏览器访问某一个网站的时候,都会附上该网站之前存在浏览器本地的Cookie(如果有的话)。比如登录的时候,你可以通过Cookie记录用户的登录账户,不需要用户再次输入用户名了。 在v1目录新建`setCookie.php`,来做个Cookie的小实验: ```php 标签之前。 // expire为有效期,单位为秒 // setcookie(name, value, expire, path, domain); // 设置name:zhangsan ,有效期一分钟 setcookie('name', 'zhangsan', time()+60); echo 'set cookie done!'; ?> ``` 新建`getCookie.php`: ```php ``` 首先访问`getCookie.php`,此时的请求内容和相应内容为: ![](/images/2018/04/3526611667.png) 然后访问`setCookie.php`: ![](/images/2018/04/1285454816.png) 这时候可以看到响应头里面设置了cookie,我们再次访问`getCookie.php`: ![](/images/2018/04/2665486333.png) 如果你没有看到请求的时候携带的Cookie,证明60s到啦,你可以在请求一次`setCookie.php`,然后再来看请求的头。无论请求哪个页面,只要是当前`域`下的页面,Cookie都会带上。 ### 什么是 Session 通俗来讲,Session是用来`记录会话信息`的,比如记录用户的登录状态,记录用户的购物车。Session的原理是(通俗的说): (1)Session启用时,会生成一个唯一的标识id(我记作SessionId),存储在浏览器客户端的Cookie中。言外之意,浏览器如果禁止Cookie那么Sesson也用不了了。 (2)服务端会存储某个SessionId对应的会话信息,PHP默认存储在硬盘上。比如我用户登录的时候,登录成功,我把用户的用户名存在Session里。那么下次用户带着SessionId访问这个页面时,我根据这个Id就能知道会话都存了什么信息。 新建`user.php`: ```php 标签之前 session_start(); if(isset($_SESSION['username'])){ // 一般存储user的id,根据id查询数据库获取更多用户信息 echo '已经登录'; echo '这个页面只能登陆后看到'; }else{ echo '请先登录'; } ?> ``` 修改`login.php` ```php 标签之前 session_start(); // isset函数检测变量是否设置,并且不是 NULL。 // 这里用来判断是不是POST请求 if(isset($_POST['username'])){ // 如果是,把POST的数据取出来 $username = $_POST['username']; $password = $_POST['password']; // 系统里肯定是查数据库,判断用户是否存在,密码对不对等等 // 这里简单写死,判断 if($username=="root" && $password == "root"){ // 设置Session $_SESSION['username'] = 'root'; echo "登录成功"; } // 注意,这里没写if的另一个中括号 ?> 用户名: 密 码: 登陆后可见的页面 登录 ``` 首先访问`user.php`看看: ![](/images/2018/04/4283747021.png) 此时的请求为: ![](/images/2018/04/4209514793.png) 还记得**Session启用时,会生成一个唯一的标识id**不,这里启用Session对应代码第六行`session_start()`,看到服务端返回了一个Cookie`PHPSESSID`: ![](/images/2018/04/3425571239.png) 这个时候呢,已经启用了Session,但是还没有在这个会话中保存任何信息。 然后在登录页面`login.php`,输入用户名密码,进行登录: ![](/images/2018/04/241419353.png) 这个时候已经带着SessionId请求服务端了。 SessionId是`PHPSESSID=c9d70faae1db743256c82edcbdbf6f8d`,我先记下来,一会用。 登录成功后是不是可以访问`user.php`了? ![](/images/2018/04/1040589488.png) 我用之前提到的抓包工具`Fiddler`模拟提交一次请求,访问`user.php`: ![](/images/2018/04/4063330136.png) 请求结果: ![](/images/2018/04/3160653282.png) 提示我需要登录,我用之前记下来的SessionId,在请求中构造以下Cookie: ![](/images/2018/04/1434711247.png) 请求结果: ![](/images/2018/04/2653958490.png) 没毛病,服务端认这个SessionId,已经输出只有登录才能看到的页面了。 假如有恶意程序提取了你某网站的Cookie,那么就可以绕过登录,以你的身份访问网站了。 假如你要写个爬虫爬取教务系统中你的成绩,那么先用爬虫爬取登录页面,模拟登录,然后拿到Cookie(包含了SessionId),之后带着拿到的Cookie去请求需要登录后才能访问的页面,这样就能获取的你的成绩等信息了。 总算说完了Session,还有最后一个部分就OK了。 ### Token 鉴权 因为完整的实现要牵涉到使用数据库,而我又没给你介绍数据库,所以这里不写代码了(我懒我懒我懒),介绍一下思想。 你也算做过Vue + Element UI 的工程了,前后端是完全分离的,页面不经服务端渲染,你没有拿到SessionId的机会。或者是个第三方服务,你要调用别人的接口,但是别人在你调用接口的时候需要知道是谁在调用,你有没有权限调用。这个时候就可以通过Token鉴权来实现了。整体思路: (1)用户名/密码 或者 APPID/APPSECRET 换取 Token (2)拿着 Token 去请求接口 (3)注意 Token 的有效期 具体生成Token的算法和思路,很成熟,这里先不介绍了。 举个例子,通过Token获取用户信息,有以下2个接口: 接口:`getToken` 参数:`username`和`password` 请求方式:`GET` 返回: `{"access_token":"xxxxxxx","expires_in":7200}` 接口:`getInfo` 参数:无 请求方式:`GET` 成功返回:`{"name":"xxx","age":"20","sex":"人妖"}` 怎么用呢?如果直接`GET` `getInfo` ,可能返回`{"error":"true","msg":"你没有权限访问该接口"}`。 正确的方式呢: (1)首先请求`getToken?username=root&password=root`,拿到Token。 (2)请求`getInfo?token=xxxx`,获取个人信息。 注意Token有没有在有效期呀等等。一般,你的服务端开发人员或者第三方服务会给你一个`API 列表`,详细的描述了怎样调用他们封装的BUG。 ### 写在最后 如何快速使用一个WEB框架?我一般会重点看他的`路由功能`,如何`获取GET/POST请求内容`,如何设置`Cookie`,如何设置`Session`,如何`返回JSON`等。当然有时间的话,从头完整的多看几遍文档,理解他的设计思想和特色最好啦。