HTTP 协议在设计之初,为了保持简单,本身是没有状态的,也就是说,对同一个客户端浏览器而言,上一次对服务器的请求和下一次请求之间是完全独立的、互不关联的,在服务器端并不能识别两次请求是同一个浏览器发起的,在不改变 HTTP 协议本身设计的前提下,为了解决这个问题,引入了 Cookie 技术来管理服务器与客户端之间的状态。
Cookie 是服务器发送到客户端浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上,以此来实现客户端识别和状态管理。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议实现状态管理成为了可能。
Cookie 主要的应用场景如下:
我们可以在浏览器中通过控制台或者第三方插件很轻松的查看某个站点的所有 Cookie 信息,以
「学院君」网站首页为例,通过 Chrome 控制台的 Application->Storage->Cookies 导航可以看到 https://xueyuanjun.com
这个域名下的所有 Cookie:
或者通过 Chrome 商店下载的管理站点 Cookie 的 EditThisCookie 插件查看当前站点的 Cookie 信息,使用这个插件的好处是可以对 Cookie 进行修改和设置:
需要注意的是,为了安全起见,Cookie 遵循浏览器同源策略,即不同站点(域名)之间不共享 Cookie,在一个站点下不能访问另一个站点的 Cookie,Cookie 必须和域名绑定,设置在指定域名下的 Cookie 只能在该域名下访问。
在上面的 Cookie 展示中,可以看到有些 Cookie 是设置在 .xueyuanjun.com
域名下的,有些是设置在根域名 xueyuanjun.com
下的,两者之间的区别是前者的 Cookie 可以被子域名站点访问,例如 www.xueyuanjun.com
、admim.xueyuanjun.com
等,当然也包括 xueyuanjun.com
本身,但是后者的 Cookie 只能被 xueyuanjun.com
站点访问,其他子域名不能访问其 Cookie。
注:更多关于 HTTP Cookie 的技术细节及实现原理,可以参考 MDN 上的 HTTP Cookies 或者阅读学院君在网络协议篇中编写的 HTTP 报文首部字段(五):扩展字段篇(Cookie) 这篇教程。
Cookie 是 HTTP 协议层面的技术,与具体语言无关,要发送 Cookie 到客户端,可以通过在响应头中设置 Set-Cookie
头来实现。在 PHP 中,可以通过 header
函数来发送所有响应头,不过,由于 Cookie 有很多额外属性,使用该方法操作未免过于繁琐,而且代码可读性和可维护性较差,为此,PHP 提供了一个专门用于发送 Cookie 到客户端的函数 —— setcookie:
通过 setcookie
设置的 Cookie 会和已有的 Cookie 一起设置到 Set-Cooki
e 响应头和 HTTP 响应一起发送给客户端,如果请求头中已经包含同名 Cookie,则覆盖之。
在 setcookie 函数中,第一个参数 name 是 Cookie 的名称,第二个参数 value 是对应的 Cookie 值,接下来的几个参数是 Cookie 的属性:
$expire
表示该 Cookie 的过期时间,默认随着浏览器关闭而失效;$path
表示该 Cookie 的服务器路径,默认是 /
,表示对整个域名有效,否则是配置域名的指定目录下有效(一般留空使用默认值即可);$domain
表示该 Cookie 所属的域名,默认是调用该函数的应用所属域名;$secure
表示该 Cookie 是否仅仅通过安全的 HTTPS 连接传给客户端,默认是 false
,即 HTTP 连接也有效;$httponly
表示该 Cookie 只能通过 HTTP 协议访问,如果在命令行调用无效,默认是 false
。下面我们调用这个函数来发送 Cookie 到客户端,在 php_learning/http
目录下新建 cookie.php
来保存本篇教程的代码:
<?php
setcookie('name', '学院君');
$expires = time() + 3600;
setcookie('website', 'https://xueyuanjun.com', $expires); // 1 小时后过期
echo '设置 Cookie 成功';
然后,我们在浏览器中访问 http://localhost:9000/cookie.php
:
可以看到响应头中已经包含两个用于设置 Cookie 的 Set-Cookie
响应头,第二个 Cookie 还包含了过期信息( PHP 底层将过期信息转化为 expires
和 Max-Age
两个属性,前者表示具体过期时间点,后者表示剩余过期时间)。
在 EditThisCookie 扩展中也可以看到对应的站点 Cookie 信息了:
在 PHP 中,可以通过超全局变量 $_COOKIE
来获取请求中的 Cookie 信息,通过访问关联数组的方式访问指定名称的 Cookie 值即可:
$name = $_COOKIE['name'];
$website = $_COOKIE['website'];
但是需要注意的是,本次响应发送的 Cookie 需要在下次请求时才能在服务端获取到,这很好理解,因为 Cookie 是随着响应头发送到客户端,再由客户端下次请求时自动在请求头中带上 Cookie 信息对服务器发起请求,服务器通过解析请求头才能获取到上次发送给客户端的 Cookie。
因此,我们在设置完 Cookie 后,接着直接访问肯定获取不到,因此此时请求头中并没有对应的 Cookie 信息(除非上次响应发送了同名 Cookie)。
我们改写上述 cookie.php
的实现,新增读取 Cookie 的逻辑:
<?php
if (isset($_GET['action']) && $_GET['action'] == 'get_cookies') {
$name = $_COOKIE['name'];
$website = $_COOKIE['website'];
printf('从用户请求中读取的 Cookie 数据:{name: %s, website: %s}', $name, $website);
exit();
}
setcookie('name', '学院君');
$expires = time() + 3600;
setcookie('website', 'https://xueyuanjun.com', $expires); // 1 小时后过期
header('Location: /cookie.php?action=get_cookies');
设置完 Cookie 后将用户重定向到 http://localhost:9000/cookie.php?action=get_cookies
,相当于重新对服务端发起请求,这一次,由于客户端浏览器已经包含了相关的 Cookie,所以就可以读取到对应的数据信息了:
服务端正是从客户端请求头的 Cookie 字段中解析出的 Cookie 数据。
更新 Cookie 还是调用 setcookie
函数,设置同名 Cookie,然后修改属性值覆盖之前的设置即可,在 cookie.php
中新增更新 Cookie 代码:
<?php
// 获取 Cookie
if (isset($_GET['action']) && $_GET['action'] == 'get_cookies') {
$name = $_COOKIE['name'];
$website = $_COOKIE['website'];
printf('从用户请求中读取的 Cookie 数据:{name: %s, website: %s}', $name, $website);
exit();
}
// 更新 Cookie
if (isset($_GET['action']) && $_GET['action'] == 'set_cookies') {
$expires = time() + 3600 * 24;
setcookie('name', '学院君', $expires); // 设置过期时间为 1 天
echo '更新 Cookie 成功';
exit();
}
// 首次访问添加 Cookie
setcookie('name', '学院君');
$expires = time() + 3600;
setcookie('website', 'https://xueyuanjun.com', $expires); // 1 小时后过期
header('Location: /cookie.php?action=get_cookies');
我们更新了 name
对应的 Cookie 有效期为 1 天,访问 http://localhost:9000/cookie.php?action=set_cookies
,可以看到过期时间已经调整:
由于 Cookie 实际上是保存在客户端的,所以服务端无法主动删除 Cookie,只能通过某种机制告知客户端删除该 Cookie,显然,通过服务端设置该 Cookie 已过期是个不错的方案,客户端在判定该 Cookie 已过期后会主动将其删除。
我们可以通过 setcookie
函数设置要删除的 Cookie 过期时间为过去的时间,这样响应发送到客户端后,客户端判定该 Cookie 已过期,然后主动将其删除:
... // 获取和更新 Cookie
// 删除 Cookie
if (isset($_GET['action']) && $_GET['action'] == 'del_cookies') {
$expires = time() - 1;
setcookie('website', '', $expires); // 通过设置过期时间为过去的时间让客户端主动删除对应 Cookie
echo '删除 Cookie:website';
exit();
}
... // 新增 Cookie
在浏览器中访问 http://localhost:9000/cookie.php?action=del_cookies
,可以看到响应头中 Set-Cookie
已经将 website
标识为已删除,过期时间也是 Unix 元年(过去的时间):
通过 EditThisCookie 扩展也可以看到 Cookie 列表中 website
已经不复存在了:
当然,在服务端通过 $_COOKIE['website']
也无法访问到它了,不仅如此,还会抛出一个 Notice 级别的错误,因为对应的关联数组索引不存在:
通过上面的介绍,想必你已经对 Cookie 的基本原理和增删改查实现有了大致的了解,首先 Cookie 需要在服务端生成,然后通过 Set-Cookie
响应头发送给客户端浏览器,客户端浏览器会将服务器发送过来的 Cookie 数据存储在本地,并根据过期时间对其进行维护,已过期的 Cookie 会自动删除,未设置过期时间的 Cookie 生命周期随着浏览器关闭而终结(这种 Cookie 也可以称之为 Session Cookie),然后下次客户端向服务端发起请求时,就会带上对应域名作用域下的所有 Cookie,服务端根据这些 Cookie 可以感知客户端信息,从而实现 HTTP 状态管理,实际上 Session 技术正是基于存储在 Cookie 中的 Session ID 实现对用户登录状态的管理的,所不同的是,Session 数据是存储在服务端的,然后通过客户端 Session ID 识别并维护用户认证状态。下篇教程,我们来探讨 Session 技术的实现原理和增删改查的实现。