HTTP 协议本身是无状态的,在 PHP 中使用 Session (会话)来保存某些数据,以便在后续进行访问,这让基于会话的数据共享成为可能。
用户在访问网站时,会被分配一个唯一的 id,这就是 Session ID (会话 ID)。它有两种传递方式,一是存储在客户端的 Cookie 中,二是在 url 中作为参数。
会话技术支持你把请求中的数据保存到超全局数组 $_SESSION
中。当用户访问网站时,PHP 将自动检查(如果 session.auto_start
被设置成 1,即开启状态)或者手动检查(用 session_start()
开启)会话 ID 是否由之前发送的请求所创建。如果是的,那么之前保存的会话将被重新建立。
如果开启了
session.auto_start
,那么将对象放入会话的唯一方法是使用auto_prepend_file
来加载定义这个对象的类,并使用serialize()
和unserialize()
来对其进行序列化和反序列化操作。
请求结束时,PHP 内部会将 $_SESSION
(以及所有已注册的变量)用指定的序列化处理程序进行序列化处理,该配置可以在 ini 配置文件中的 session.serialize_handler
进行设置。已注册但未定义的变量将被标记为未定义。在后续访问时,这些变量不会被会话模块定义,除非用户后来定义了它们。
注意:因为会话数据是序列化后保存的,所以资源类型的变量不能被保存在会话中。
序列化处理程序(php 和 php_binary)受限于
register_globals
。因此,数字索引,或者包含有特殊字符(|
和!
)的字符串索引不能使用,不然将会产生错误而使程序停止运行。php_serializa
没有此类限制,在 PHP 5.5.4 之后可用。当使用会话时,只有当变量使用
session_register()
注册时或者添加新的元素到$_SESSION
超全局数组后,会话记录才会被创建。不管有没有使用session_start()
来开启会话。默认情况下,所有与特定会话相关的数据都被存储在由 ini 选项
session.save_path()
指定的目录下的一个文件中。每个会话会建立一个文件(无论是否有数据与该会话有关)。这是由于每打开一个会话即建立一个文件,无论是否有数据写入到该文件中。注意由于和文件系统协同工作的限制,此行为有个副作用,有可能造成用户指定的会话处理器(如用数据库)丢失了未存储数据的会话。
会话是用来为单个用户以会话 ID 来存储数据的一种简单方式。它可以被用来在多个页面请求之间,保存和共享用户状态信息。一般来说,会话 ID 通过 Cookie 的方式发送到浏览器,并且在服务器端也是通过会话 ID 来取回会话中的数据。如果请求中不包含会话 ID 或者会话 Cookie,那么 PHP 就会创建一个新的会话,并为新创建的会话分配一个新的会话 ID。
会话的工作流程很简单。当开启一个新的会话时,PHP 会尝试用传过来的会话 ID (通常来自于会话 Cookie)去取会话数据,如果不包含会话 ID 或没找到会话数据,就创建一个新的会话。会话开启之后,PHP 就会将会话中的保存的数据保存到 $_SESSION
变量中。当 PHP 结束运行的时候,它会自动读取 $_SESSION
中的数据,进行序列化操作,然后发送给会话保存处理器进行保存。
默认情况下,PHP 使用内置的文件(files)会话保存处理器来完成会话的保存。可以通过配置项 session.save_handler
来指定要采用的会话保存处理器。它会将会话数据保存在服务器上,具体路径由 session.save_path
指定。
可以通过 session_start()
来手动开启一个会话。如果配置项 session.auto_start
值为 1,那么请求开始时,会话会自动创建。
PHP 脚本执行完毕之后,会话会自动关闭。同时,也可以通过 session_write_close()
来手动关闭。
/*
* 在 $_SESSION 中注册变量
*/
session_start();
if (!isset($_SESSION['count'])) {
$_SESSION['count'] = 0;
} else {
$_SESSION['count']++;
}
/*
* 在 $_SESSION 中移除变量
*/
session_start();
unset($_SESSION['count']);
名字 | 默认值 | 可修改范围 | 简介 |
---|---|---|---|
session.save_path | "" | PHP_INI_ALL | 默认 files 文件处理器,指定存储路径 |
session.name | "PHPSESSID" | PHP_INI_ALL | 会话名,作为 Cookie 的名字 |
session.save_hander | "files" | PHP_INI_ALL | 指定会话保存处理器 |
session.auto_start | "0" | PHP_INI_PERDIR | 自动开启会话 |
session.gc_probability | "1" | PHP_INI_ALL | 会话回收概率 |
session.gc_divisor | "100" | PHP_INI_ALL | 会话回收概率被除数,也就是基数 |
session.gc_maxlifetime | "1440" | PHP_INI_ALL | 会话最大生命时间(秒),超过该时间后会被视为「垃圾」 |
session.serialize_handler | "php" | PHP_INI_ALL | 会话序列化处理程序名称 |
session.cookie_lifetime | "0" | PHP_INI_ALL | 会话 Cookie 生命周期,0 为关闭浏览器时 |
session.cookie_path | "/" | PHP_INI_ALL | 会话 Cookie 作用路径 |
session.cookie_domain | "" | PHP_INI_ALL | 会话 Cookie 作用域名 |
session.cookie_secure | "" | PHP_INI_ALL | 会话 Cookie 是否只支持 HTTPS |
session.cookie_httponly | "" | PHP_INI_ALL | 会话 Cookie 是否限制不让 javascript 获取 |
session.use_strict_mode | "0" | PHP_INI_ALL | 严格模式,开启后只接受自身初始化的会话 ID |
session.use_cookie | "1" | PHP_INI_ALL | 是否用 Cookie 来存放会话 ID |
session.use_only_cookie | "1" | PHP_INI_ALL | 是否限制不允许 URL 传递会话 ID |
session.referer_check | "" | PHP_INI_ALL | 指定允许的会话 HTTP Referer,未找到该字串,则会话无效 |
session.entropy_file | "" | PHP_INI_ALL | 会话 ID 创建时附加的熵值 |
session.entropy_length | "0" | PHP_INI_ALL | 读取熵值的字节数,0 为禁用 |
session.cache_limiter | "nocache" | PHP_INI_ALL | 指定会话页面使用的缓冲控制方法 |
session.cache_expire | "180" | PHP_INI_ALL | 指定缓冲页面的存活期(分),对 nocache 无效 |
session.use_trans_sid | "0" | PHP_INI_ALL | 是否启用透明 SID 支持 |
session.hash_function | "0" | PHP_INI_ALL | 指定生成会话 ID 的散列算法 |
session.upload_progress.enable | "1" | PHP_INI_PREDIR | 是否开启上传进度检测 |
session.lazy_write | "1" | PHP_INI_ALL | 开启时,会话数据只有在被更改时才被重新写入 |
会话是有生命周期的,也就是存活期,默认情况下是 24 分钟后即失效。会话失效后,会被标记为「垃圾」,由 GC (Garbage collection)进行回收处理。但 GC 并不是一直运行的,而是有一定概率被触发启动并运行,启动概率和 session.gc_probability
和 session.gc_divisor
的值有关,比如默认值依次为 1 和 100,概率就为 1%,意思是会话开启时,有 1% 的概率触发 GC 启动然后进行「垃圾会话」清理工作。所以当会话超出生命周期时,会话不一定会失效,而是要等到 GC 清理该会话后,会话才真正失效。
如果不同的脚本具有不同的
session.gc_maxlifetime
数值但是共享了同一地方存储会话数据,则具有最小数值的脚本会被清理数据。此情况下,与session.save_path
一起使用本指令。
最常见的回答是:设置 Session 的过期时间,也就是 session.gc_maxlifetime
。
这种回答是不正确的,原因如下:
首先,PHP 是用一定的概率来运行 Session 的 GC 的,不能保证到 30 分钟的时候一定会过期。
那设置一个大概率的清理概率呢?还是不妥,为什么?因为 PHP 使用 Session 文件的修改时间来判断是否过期,如果增大这个概率,一来会降低性能;二来,PHP 使用「一个」文件来保存和一个会话相关的 Session 变量。假设 5 分钟前设置了一个 a=1
的 Session 变量,5 分钟后又设置了一个 b=2
的 Session 变量,这个 Session 文件的修改时间为添加 b 时刻的时间,那么 a 就不能在 30 分钟的时候,被清理掉了。
PHP 默认(Linux 为例)使用 /tmp
作为 Session 的存储目录。也就是说,如果两个应用都没有指定自己独立的 save_path
,一个设置了过期时间 2 分钟(A),一个设置为 30 分钟(B),那么每次当 A 的 Session GC 运行的时候,就会删除属于应用 B 的 Session 文件。
所以,第一种答案不是「完全严格」正确的。
还有一种常见的答案:设置会话 ID 的载体,也就是 Cookie 的过期时间,即 session.cookie_lifetime
。这种回答也是不正确的,原因如下:
这个过期只是 Cookie 过期,换个说法这点就考察 Cookie 和 Session 的区别,Session 过期是服务器过期,而 Cookie 过期是客户端(浏览器)来保证的。即使你设置了 Cookie 过期,也只能保证标准浏览器到期的时候,不会发送这个 Cookie (包含有 Session ID),而如果通过构造请求,还是可以使用这个 Session ID 的值。
使用 Memcache、Redis 等,ok,这种答案似乎是一种正确答案。不过,很显然出题者肯定还会接着问你,如果只是使用 PHP 呢?
当然,面试不是为了难为你,而是为了考察思考的周密性。一般来说,符合题意的做法是:
设置 Cookie 过期时间 30 分钟,并设置 Session 的 lifetime 也是 30 分钟
自己为每一个 Session 值增加 timestamp
每次访问前,判断时间戳
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > 1800)) {
session_unset();
session_destroy();
}
$_SESSION['last_activity'] = time();
最后,有同学问,为什么要设置 30 分钟的过期时间。首先,是为了面试;其次,实际使用场景的话,比如 30 分钟就过期的优惠券?
如果需要在数据库或 Memcached、Redis 中存储会话数据,需要使用 session_set_save_handler()
函数来创建一系列用户级存储函数。PHP 5.4.0 之后,可以通过实现 SessionHandlerInterface
接口或者继承 SessionHandler
类来扩展内置的管理器,从而达到自定义会话保存机制的目的。
一个基本的 MongoDB 实现看起来应该像这样:
namespace App\Extensions;
class MogoHandler implements SessionHandlerInterface
{
public function open($savePath, $sessionName) {}
public function close() {}
public function read($sessionId) {}
public function write($sessionId, $data) {}
public function destroy($sessionId) {}
public function gc($lifetime) {}
}
gc()
能销毁 $lifetime
之前的所有数据,$lifetime
是一个 UNIX 时间戳。对本身拥有过期机制的系统,如 Memcached 和 Redis 而言,该方法可以留空。