psr7-sessions/storageless

无存储 PSR-7 会话支持

9.1.0 2023-11-20 14:53 UTC

README

Mutation testing badge Type Coverage Packagist Packagist

PSR7Session 是一个与 PSR-7PSR-15 兼容的 中间件,允许在基于 PSR-7 的应用程序中不使用 I/O 使用会话。

ocramiusmalukenholcobucci 带来。

安装

composer require psr7-sessions/storageless

使用方法

您可以在任何 PSR-15 兼容的中间件中使用 PSR7Sessions\Storageless\Http\SessionMiddleware

在一个 mezzio/mezzio 应用程序中,这看起来是这样的

use Lcobucci\JWT\Configuration as JwtConfig;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key\InMemory;
use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Http\Configuration as StoragelessConfig;

$app = new \Mezzio\Application(/* ... */);

$app->pipe(new SessionMiddleware(
    new StoragelessConfig(
        JwtConfig::forSymmetricSigner(
            new Signer\Hmac\Sha256(),
            InMemory::base64Encoded('OpcMuKmoxkhzW0Y1iESpjWwL/D3UBdDauJOe742BJ5Q='), // replace this with a key of your own (see below)
        )
    )
));

之后,您可以在任何具有对 Psr\Http\Message\ServerRequestInterface 属性访问权限的中间件中访问会话数据

$app->get('/get', function (ServerRequestInterface $request, ResponseInterface $response) : ResponseInterface {
    /** @var \PSR7Sessions\Storageless\Session\SessionInterface $session */
    $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
    $session->set('counter', $session->get('counter', 0) + 1);

    $response
        ->getBody()
        ->write('Counter Value: ' . $session->get('counter'));

    return $response;
});

由于不涉及超级全局变量和 I/O,因此您也可以在异步上下文和长时间运行的进程中这样做。

建议您使用具有大量熵的密钥,最好使用加密安全的伪随机数生成器 (CSPRNG) 生成。您可以使用 CryptoKey 工具 来完成此操作。

请注意,您还可以使用非对称密钥;请参阅 lcobucci/jwt 文档

  1. 配置对象: https://lcobucci-jwt.readthedocs.io/en/stable/configuration/
  2. 支持算法: https://lcobucci-jwt.readthedocs.io/en/stable/supported-algorithms/

会话劫持缓解

为了缓解与 cookie 盗窃相关的风险,以及由此产生的 会话劫持,您可以通过启用客户端指纹识别将用户会话绑定到其 IP($_SERVER['REMOTE_ADDR'])和 User-Agent($_SERVER['HTTP_USER_AGENT']

use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Http\Configuration as StoragelessConfig;
use PSR7Sessions\Storageless\Http\ClientFingerprint\Configuration as FingerprintConfig;

$app = new \Mezzio\Application(/* ... */);

$app->pipe(new SessionMiddleware(
    (new StoragelessConfig(/* ... */))
        ->withClientFingerprintConfiguration(
            FingerprintConfig::forIpAndUserAgent()
        )
));

如果您的 PHP 服务背后有您的反向代理,您可能需要从不同的真实来源检索客户端 IP。在这种情况下,您可以编写自定义的 \PSR7Sessions\Storageless\Http\ClientFingerprint\Source 实现来提取您所需的信息

use Psr\Http\Message\ServerRequestInterface;
use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Http\Configuration as StoragelessConfig;
use PSR7Sessions\Storageless\Http\ClientFingerprint\Configuration as FingerprintConfig;
use PSR7Sessions\Storageless\Http\ClientFingerprint\Source;

$app = new \Mezzio\Application(/* ... */);

$app->pipe(new SessionMiddleware(
    (new StoragelessConfig(/* ... */))
        ->withClientFingerprintConfiguration(
            FingerprintConfig::forSources(new class implements Source{
                 public function extractFrom(ServerRequestInterface $request): string
                 {
                     return $request->getHeaderLine('X-Real-IP');
                 }
            })
        )
));

示例

只需在控制台浏览到您的 examples 目录,然后运行

php -S localhost:9999 index.php

然后尝试访问 http://localhost:9999:您应该看到一个在每次刷新页面时增加的计数器

为什么?

在大多数 PHP+HTTP 相关项目中,ext/session 都能发挥其作用,并允许我们通过将特定标识符与访问用户代理关联来存储服务器端信息。

ext/session 存在什么问题?

这一切都很公平,也很美好,除了

  • 依赖于 $_SESSION 超全局变量
  • 依赖于关闭处理程序以将会话“提交”到存储中
  • 由于存储,活动用户数量存在巨大限制
  • 由于存储,存在大量I/O操作
  • 由于存储,不同进程之间存在序列化数据(PHP为您序列化和反序列化$_SESSION,并且存在安全影响)
  • 对于水平扩展的设置,必须使用集中式存储
  • 当存储不是集中式时,必须使用粘性会话(带有“智能”负载均衡器)
  • 未设计用于多个调度周期

这个项目做什么?

此项目尝试实现无存储会话并缓解上述问题。

假设

  • 您的会话相对较小,只包含少量标识符和一些CSRF令牌。小意味着< 400字节
  • 会话中的数据是JsonSerializable或等效的
  • 会话中的数据可以由客户端自由读取

它是如何工作的?

会话数据直接存储在会话cookie中作为JWT令牌。

这种方法并不新颖,通常与HTTP/REST/OAuth API中的Bearer令牌一起使用。

为了确保会话数据未被修改,客户端可以信任信息,并且服务器和客户端之间达成协议的过期日期,使用JWT令牌来传输信息。

JWT令牌始终进行签名,以确保用户代理永远无法操纵会话。支持对称和非对称密钥进行签名/验证令牌。

优点

  • 无需存储
  • 无需粘性会话(任何拥有私有或公共密钥副本的服务器都可以生成会话或消费它们)
  • 可以向客户端传输明文信息,允许它与服务器共享一些信息(一个标准示例是在给定的会话中共享“用户名”或“用户ID”)
  • 可以向客户端传输加密信息,允许服务器仅消耗信息
  • 不受PHP序列化RCE攻击的影响
  • 不受PHP进程作用域的限制:每个进程可以有多个会话
  • 不依赖于全局状态
  • 在多服务器设置中,您可能允许只有访问公共密钥的服务器进行只读访问,而写入操作仅限于可以访问私有密钥的服务器
  • 可以在多个调度周期中使用

配置选项

请参阅配置文档

已知限制

请参阅限制文档

贡献

请参阅贡献说明

许可

本项目在MIT许可下公开。