PHP 会话机制

老牛浏览 347评论 0发表于

1. 简介

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() 指定的目录下的一个文件中。每个会话会建立一个文件(无论是否有数据与该会话有关)。这是由于每打开一个会话即建立一个文件,无论是否有数据写入到该文件中。注意由于和文件系统协同工作的限制,此行为有个副作用,有可能造成用户指定的会话处理器(如用数据库)丢失了未存储数据的会话。

2. 基本使用

会话是用来为单个用户以会话 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() 来手动关闭。

php
/*
 * 在 $_SESSION 中注册变量
 */
session_start();
if (!isset($_SESSION['count'])) {
    $_SESSION['count'] = 0;
} else {
    $_SESSION['count']++;
}
php
/*
 * 在 $_SESSION 中移除变量
 */
session_start();
unset($_SESSION['count']);

3. 常用配置项

名字

默认值

可修改范围

简介

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

开启时,会话数据只有在被更改时才被重新写入

4. 垃圾回收

会话是有生命周期的,也就是存活期,默认情况下是 24 分钟后即失效。会话失效后,会被标记为「垃圾」,由 GC (Garbage collection)进行回收处理。但 GC 并不是一直运行的,而是有一定概率被触发启动并运行,启动概率和 session.gc_probabilitysession.gc_divisor 的值有关,比如默认值依次为 1 和 100,概率就为 1%,意思是会话开启时,有 1% 的概率触发 GC 启动然后进行「垃圾会话」清理工作。所以当会话超出生命周期时,会话不一定会失效,而是要等到 GC 清理该会话后,会话才真正失效。

如果不同的脚本具有不同的 session.gc_maxlifetime 数值但是共享了同一地方存储会话数据,则具有最小数值的脚本会被清理数据。此情况下,与 session.save_path 一起使用本指令。

5. 深入理解

5.1 如何设置一个严格 30 分钟过期的会话

第一种答案

最常见的回答是:设置 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 呢?

第四种答案

当然,面试不是为了难为你,而是为了考察思考的周密性。一般来说,符合题意的做法是:

  1. 设置 Cookie 过期时间 30 分钟,并设置 Session 的 lifetime 也是 30 分钟

  2. 自己为每一个 Session 值增加 timestamp

  3. 每次访问前,判断时间戳

php
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > 1800)) {
    session_unset();
    session_destroy();
}
$_SESSION['last_activity'] = time();

最后,有同学问,为什么要设置 30 分钟的过期时间。首先,是为了面试;其次,实际使用场景的话,比如 30 分钟就过期的优惠券?

5.2 自定义会话管理器

如果需要在数据库或 Memcached、Redis 中存储会话数据,需要使用 session_set_save_handler() 函数来创建一系列用户级存储函数。PHP 5.4.0 之后,可以通过实现 SessionHandlerInterface 接口或者继承 SessionHandler 类来扩展内置的管理器,从而达到自定义会话保存机制的目的。

一个基本的 MongoDB 实现看起来应该像这样:

php
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 而言,该方法可以留空。

点赞
收藏
暂无评论,快来发表评论吧~
私信
老牛@ilaoniu
老牛,俗称哞哞。单纯的九零后理工小青年。喜欢折腾,爱玩,爱音乐,爱游戏,爱电影,爱旅游...
最后活跃于