不推荐将数组数据通过 serialize() 存储到 cookie 中,因为:

Cookie 名称可以设置成数组名称,PHP 脚本里会是数组, 但用户系统里储存的是单独分开的 Cookie。 可以考虑使用 explode() 为一个 Cookie 设置多个名称和值。 不建议将 serialize() 用于此处,因为它会导致安全漏洞。

—— 注释 - setcookie - php.net

这边仅作测试使用。以下为测试 demo1:

$goods_list = array(
    "xx-aa"=>array("price"=>"25.00", "number"=>"10", "title"=>"普通毛巾"),
    "yy-bb"=>array("price"=>"36.00", "number"=>"8", "title"=>"精品毛巾"));
ob_start();
header('HTTP/1.1 200');
header('Content-Type:application/json');
var_dump($goods_list);
$goods_serial = serialize($goods_list);
var_dump($goods_serial);
set_cookie('goods_list', $goods_serial);
$goods_serial = get_cookie('goods_list');
var_dump($goods_serial);
$goods_list = unserialize($goods_serial);
var_dump($goods_list);

这份代码在服务器上跑,反序列化后结果为 false。查了 unserialize() 函数注解,传递的字符串不可解序列化会返回 false。也就是说从 cookie 中再次取出来的值与存入的值不等。

因为在本地(php 7.3.2 nts)和个人服务器(php 7.3.2 zts)测试,都是可以正常取出数组,所以可以排除线程安全问题。之后想到可能是 php 版本问题,服务器使用的是 php 5.2.17(很古老的 php 版本了),猜测是低版本环境下,setcookie 做了一些处理导致了值不同。

在 setcookie 函数注解的范例 - 案例#1 中有这样一个说明:

注意:在发送 Cookie 时,值的部分会被自动 urlencode 编码。收到 Cookie 时,会自动解码,并赋值到可变的 Cookie 名称上。 如果不想被编码,可以使用 setrawcookie() 代替。

也就是说,正常情况下 setcookie 和 getcookie 会分别使用 urlencode 编码字符串值,urldecode 解码字符串值。那么是不是低版本下缺少了这个步骤呢?测试 demo2 如下:

$goods_list = array(
    "xx-aa"=>array("price"=>"25.00", "number"=>"10", "title"=>"普通毛巾"),
    "yy-bb"=>array("price"=>"36.00", "number"=>"8", "title"=>"精品毛巾"));
ob_start();
header('HTTP/1.1 200');
header('Content-Type:application/json');
var_dump($goods_list);
$goods_serial = urlencode(serialize($goods_list));
var_dump($goods_serial);
set_cookie('goods_list', $goods_serial);
$goods_serial = get_cookie('goods_list');
var_dump($goods_serial);
$goods_list = unserialize(urldecode($goods_serial));
var_dump($goods_list);

结果数组正常正常解析出来了。然后就有了使用 json_encode 和 json_decode 版本(serialize() 存在漏洞,不安全)demo3:

$goods_list = array(
    "xx-aa"=>array("price"=>"25.00", "number"=>"10", "title"=>"普通毛巾"),
    "yy-bb"=>array("price"=>"36.00", "number"=>"8", "title"=>"精品毛巾"));
ob_start();
header('HTTP/1.1 200');
header('Content-Type:application/json');
var_dump($goods_list);
$goods_serial = urlencode(json_encode($goods_list));
var_dump($goods_serial);
set_cookie('goods_list', $goods_serial);
$goods_serial = get_cookie('goods_list');
var_dump($goods_serial);
$goods_list = json_decode(urldecode($goods_serial), true);
var_dump($goods_list);

一开始可能因为 urldecode 位置放错了,没有成功,然后观察了中间刚取出 cookie 的值,发现了 unserialize() 反序列化失败的真正原因:php 5.2.17 版本 setcookie 存入的值使用了 addslashes() 函数做转义处理,但 getcookie 取出时没有自动调用 stripslashes() 取消转义。

针对这个问题,可以在取出 cookie 之后增加一步取消处理即可:

$goods_serial = stripslashes($goods_serial);

后续 php 版本修复了这个 bug,在 cookie 读写函数内部采用了 urlencode 和 urldecode 方法处理数据。而低版本中使用 urlencode 和 urldecode 方法也可以解决这个 bug,因为 url 转码恰好修改了需要转义的项,使 addslashes() 无功而返,最终解码成功。