aliene / phalcon-session-redis
Phalcon PHP 的替代 Redis 会话处理器
Requires
- php: >=5.6
- ext-redis: *
Requires (Dev)
- guzzlehttp/guzzle: ^6.3
- phpunit/phpunit: ^7.0
This package is not auto-updated.
Last update: 2024-09-21 04:26:51 UTC
README
Phalcon PHP 的一个替代 Redis 会话处理器,具有会话锁和解固定会话保护功能。
安装
PhalconSessionRedis 需要 PHP >=5.6,启用了 phpredis 扩展,并有一个 Redis >=2.6 端点。将 aliene/phalcon-session-redis
添加到 composer.json
文件
$ composer require aliene/phalcon-session-redis
示例
// services.php require_once __DIR__ . '/../vendor/autoload.php'; $di->set("session", function() { $session = new \Aliene\Phalcon\Session\Redis([ "host" => "localhost", "auth" => "password", "lifetime" => 3600 // ttl 1 hour ]); $session->start(); });
可用选项
host
(字符串) 默认"localhost"
port
(整数) 默认6379
lifetime
(整数),默认3600
,会话生存期(ttl)prefix
(字符串),默认'SESSIONS:'
auth
(字符串),默认null
database
(整数),默认0
目前只支持单个主机定义。
已知问题
不推荐将 PhalconSessionRedis 与 max_execution_time
指令设置为 0
每当可能时,处理器都会使用 max_execution_time
指令作为会话锁的硬超时。这是一种最后手段机制,即使在 PHP 进程崩溃且处理器无法自行释放锁的情况下,也可以释放会话锁。
当 max_execution_time
设置为 0
(表示没有最大执行时间)时,这种硬超时无法使用,因为锁必须保持与运行脚本所需的时间一样长,这是一个未知的时间量。这意味着如果由于某些意外原因 PHP 进程崩溃且处理器无法释放锁,就没有安全网,并且你最终会得到一个悬垂锁,你必须通过其他方式检测并清除。
因此,当使用 PhalconSessionRedis 时,建议不要禁用 max_execution_time
。
PhalconSessionRedis 不支持 session.use_trans_sid=1
或 session.use_cookies=0
当这些指令设置为这种方式时,PHP 会从使用 cookie 切换到将会话 ID 作为查询参数传递。
PhalconSessionRedis 不能在此模式下工作。 这是设计决定的。
PhalconSessionRedis 忽略 session.use_strict_mode
指令
因为禁用严格模式(默认情况!)根本没有任何意义。PhalconSessionRedis 只能在严格模式下工作。本 README 中的“会话固定”部分解释了这意味着什么。
动机
随 phpredis 一起提供的 Redis 会话处理器已经存在几个相当严重的错误几年了,即缺乏会话级别的锁定和无法防止会话固定攻击。
此软件包提供了一个兼容的会话处理器,它基于 Redis 扩展构建,不受这些问题的影响。
会话锁定说明
在 PHP 的上下文中,“会话锁定”意味着当多个具有相同会话 ID 的请求几乎同时击中服务器时,只有一个可以运行,而其他则卡在 session_start()
里面等待。只有当第一个请求完成或显式运行 session_write_close()
时,其他之一才能继续。
当一个会话处理器没有实现会话锁定时,在大量流量下可能会出现并发问题。我将使用默认的phpredis处理器和这个简单的脚本来演示这个问题。
<?php // a script that returns the total number of // requests made during a given session's lifecycle. session_start(); if (!isset($_SESSION['visits'])) { $_SESSION['visits'] = 0; } $_SESSION['visits']++; echo $_SESSION['visits'];
首先,我们发送一个请求来设置一个新的会话。然后我们使用在Set-Cookie
头中返回的会话ID发送200个并发的、经过身份验证的请求。
$ http localhost/visit-counter.php HTTP/1.1 200 OK Cache-Control: no-store, no-cache, must-revalidate Connection: keep-alive Content-Type: text/html; charset=UTF-8 Date: Mon, 23 Jan 2017 12:30:17 GMT Expires: Thu, 19 Nov 1981 08:52:00 GMT Pragma: no-cache Server: nginx/1.11.8 Set-Cookie: PHPSESSID=9mcjmlsh9gp0conq7i5rci7is8gfn6s0gh8r3eub3qpac09gnh21; path=/; HttpOnly Transfer-Encoding: chunked 1 $ hey -n 200 -H "Cookie: PHPSESSID=9mcjmlsh9gp0conq7i5rci7is8gfn6s0gh8r3eub3qpac09gnh21;" https:///visit-counter.php All requests done. Summary: Total: 0.4033 secs Slowest: 0.1737 secs Fastest: 0.0086 secs Average: 0.0805 secs Requests/sec: 495.8509 Status code distribution: [200] 200 responses
从外表看一切正常,我们得到了预期的200个OK响应,但是如果我们查看Redis数据库,我们会看到计数器严重错误。而不是201次访问,我们看到的是一个远低于这个数字的随机数。
127.0.0.1:6379> KEYS *
1) "PHPREDIS_SESSION:9mcjmlsh9gp0conq7i5rci7is8gfn6s0gh8r3eub3qpac09gnh21"
127.0.0.1:6379> GET PHPREDIS_SESSION:9mcjmlsh9gp0conq7i5rci7is8gfn6s0gh8r3eub3qpac09gnh21
"visits|i:134;"
通过查看Redis的MONITOR
输出,我们可以看到在重载下,Redis经常连续执行两个或更多的GET
命令,因此向两个或更多不同的请求返回相同的访问次数。当这种情况发生时,所有那些不幸的请求都会将相同的访问次数传递回Redis,因此其中一些最终会丢失。例如,在这个日志片段中,你可以看到第130个请求没有被计算在内。
1485174643.241711 [0 172.21.0.2:49780] "GET" "PHPREDIS_SESSION:9mcjmlsh9gp0conq7i5rci7is8gfn6s0gh8r3eub3qpac09gnh21"
1485174643.241891 [0 172.21.0.2:49782] "GET" "PHPREDIS_SESSION:9mcjmlsh9gp0conq7i5rci7is8gfn6s0gh8r3eub3qpac09gnh21"
1485174643.242444 [0 172.21.0.2:49782] "SETEX" "PHPREDIS_SESSION:9mcjmlsh9gp0conq7i5rci7is8gfn6s0gh8r3eub3qpac09gnh21" "900" "visits|i:129;"
1485174643.242878 [0 172.21.0.2:49780] "SETEX" "PHPREDIS_SESSION:9mcjmlsh9gp0conq7i5rci7is8gfn6s0gh8r3eub3qpac09gnh21" "900" "visits|i:129;"
1485174643.244780 [0 172.21.0.2:49784] "GET" "PHPREDIS_SESSION:9mcjmlsh9gp0conq7i5rci7is8gfn6s0gh8r3eub3qpac09gnh21"
1485174643.245385 [0 172.21.0.2:49784] "SETEX" "PHPREDIS_SESSION:9mcjmlsh9gp0conq7i5rci7is8gfn6s0gh8r3eub3qpac09gnh21" "900" "visits|i:130;"
RedisSessionHandler通过为每个会话创建一个“锁定”条目来解决这个问题,这个条目一次只能由一个执行线程创建。
会话固定解释
会话固定是指作为HTTP客户端选择自己的会话ID的能力。当客户端被允许选择他们的会话ID时,恶意攻击者可能能够诱骗其他客户端使用他们已经知道的ID,然后等待他们登录并劫持他们的会话。
从PHP 5.5.2版本开始,有一个名为session.use_strict_mode
的INI指令来保护PHP应用程序免受此类攻击。当“严格模式”启用并且接收到未知的会话ID时,PHP应该忽略它并生成一个新的,就像它根本没有收到一样。不幸的是,phpredis处理器忽略了该指令,并且始终信任从HTTP请求中接收到的任何会话ID。
$ http -v https:///visit-counter.php Cookie:PHPSESSID=madeupkey GET / HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Connection: keep-alive Cookie: PHPSESSID=madeupkey <---- Host: 127.0.0.1 User-Agent: HTTPie/0.9.6 HTTP/1.1 200 OK Cache-Control: no-store, no-cache, must-revalidate Connection: close Content-type: text/html; charset=UTF-8 Expires: Thu, 19 Nov 1981 08:52:00 GMT Host: 127.0.0.1 Pragma: no-cache 1 $ redis-cli 127.0.0.1:6379> keys * 1) "PHPREDIS_SESSION:madeupkey" 127.0.0.1:6379> GET PHPREDIS_SESSION:madeupkey "visits|i:1;"
因此,RedisSessionHandler只能在严格模式下工作。它只接受已经在Redis存储中存在的外部会话ID。
测试
运行测试
为此,您需要Docker >=1.10和docker-compose >=1.8。
为了运行集成测试套件,只需输入composer test
,它将负责安装开发依赖项、设置测试容器并运行测试。
$ composer test Loading composer repositories with package information Installing dependencies (including require-dev) from lock file Nothing to install or update Generating autoload files > docker-compose -f tests/docker-compose.yml up -d tests_redis_1 is up-to-date tests_fpm56_1 is up-to-date tests_fpm71_1 is up-to-date tests_fpm70_1 is up-to-date tests_redis_monitor_1 is up-to-date tests_nginx_1 is up-to-date tests_runner_1 is up-to-date > docker exec -t tests_runner_1 sh -c "TARGET=php56 vendor/bin/phpunit" stty: standard input PHPUnit 6.2.3 by Sebastian Bergmann and contributors. ................ 16 / 16 (100%) Time: 1.39 seconds, Memory: 4.00MB OK (16 tests, 54 assertions) > docker exec -t tests_runner_1 sh -c "TARGET=php70 vendor/bin/phpunit" stty: standard input PHPUnit 6.2.3 by Sebastian Bergmann and contributors. ................ 16 / 16 (100%) Time: 1.29 seconds, Memory: 4.00MB OK (16 tests, 54 assertions) > docker exec -t tests_runner_1 sh -c "TARGET=php71 vendor/bin/phpunit" stty: standard input PHPUnit 6.2.3 by Sebastian Bergmann and contributors. ................ 16 / 16 (100%) Time: 1.08 seconds, Memory: 4.00MB OK (16 tests, 54 assertions)
针对原生phpredis处理器的测试
您可以轻松地将相同的测试套件针对原生phpredis处理器运行。
要做到这一点,请取消注释tests/webroot/visit-counter.php
中启用RedisSessionHandler的那行,FPM容器将自动选择phpredis保存处理器(写作时的版本为3.1.2)。
// session_set_save_handler(new \UMA\RedisSessionHandler(), true);
$ composer test Loading composer repositories with package information Installing dependencies (including require-dev) from lock file Nothing to install or update Generating autoload files > docker-compose -f tests/docker-compose.yml up -d tests_redis_1 is up-to-date tests_fpm56_1 is up-to-date tests_fpm71_1 is up-to-date tests_fpm70_1 is up-to-date tests_redis_monitor_1 is up-to-date tests_nginx_1 is up-to-date tests_runner_1 is up-to-date > docker exec -t tests_runner_1 sh -c "TARGET=php56 vendor/bin/phpunit" stty: standard input PHPUnit 6.2.3 by Sebastian Bergmann and contributors. ...FFF..FF...... 16 / 16 (100%) Time: 1.15 seconds, Memory: 4.00MB There were 5 failures: ~~snip~~
手动测试
配置docker-compose.yml
文件以将随机TCP端口链接到nginx容器的端口80。在运行composer env-up
或composer test
后,您可以使用docker ps
看到分配的是哪一个。有了这些知识,您可以使用标准的浏览器、cURL、wrk或类似工具直接从您的本地机器对测试Web服务器进行测试。
根据您的Docker设置,您可能需要将localhost
替换为实际运行Docker守护进程的虚拟机的IP地址。
$ curl -i localhost:32768/visit-counter.php?with_custom_cookie_params HTTP/1.1 200 OK Server: nginx/1.13.1 Date: Sat, 15 Jul 2017 09:40:25 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Set-Cookie: PHPSESSID=tqm3akv0t5gdf25lf1gcspn479aa989jp0mltc3nuun2b47c7g10; expires=Sun, 16-Jul-2017 09:40:25 GMT; Max-Age=86400; path=/; secure; HttpOnly Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Pragma: no-cache 1