snicco/session

适用于 $_SESSION 无法使用的环境的独立会话实现。

v2.0.0-beta.9 2024-09-07 14:27 UTC

README

codecov Psalm Type-Coverage Psalm level PhpMetrics - Static Analysis PHP-Versions

目录

  1. 动机
  2. 安装
  3. 使用
    1. 配置
    2. 创建序列化器
    3. 驱动器
    4. 创建会话管理器
    5. 启动会话
    6. 不可变会话
    7. 可变会话
    8. 访问嵌套数据
    9. 闪存消息 / 旧输入
    10. 加密会话数据
    11. 保存会话
    12. 设置会话cookie
    13. 基于用户ID管理会话
    14. 垃圾回收
  4. 贡献
  5. 问题和PR
  6. 安全性

动机

虽然 PHP 的原生 $_SESSION 对于大多数用例都很好,但在某些环境中并不理想。其中两个是分布式 WordPress 代码PSR7/PSR15 应用程序。

Snicco 项目会话 组件是一个完全独立的库,不依赖于任何框架。

功能

  • 自动处理 失效轮换空闲超时

  • 非阻塞。

  • 跟踪会话是否已更改,并且只有在需要时才更新(而不影响超时)。

  • 只接受服务器端生成的会话 ID。

  • 支持许多存储后端,全部在各自的 composer 包中。

  • 使用 paragonie 的分令牌方法 来防止 基于时间的旁路攻击

  • 设计上安全,通过破坏存储后端(假设只读访问)无法劫持会话 ID。

  • PSR-7/15 兼容。没有对 PHP 超全局变量的隐藏依赖。

  • 区分 可变不可变 会话对象。

  • 选择将您的会话数据 json_encodingserializing。或者提供您自己的规范化程序。

  • 支持加密和解密会话数据(通过接口,别担心)。

  • 基于用户 ID 的高级会话管理。

  • 支持闪存消息和旧输入。

  • 100% 测试覆盖率和 100% psalm 类型覆盖率。

安装

composer require snicco/session

使用

创建会话配置

use Snicco\Component\Session\ValueObject\SessionConfig;

$configuration = new SessionConfig([
    // The path were the session cookie will be available
    'path' => '/',
    // The session cookie name
    'cookie_name' => 'my_app_sessions',
    // This should practically never be set to false
    'http_only' => true,
    // This should practically never be set to false
    'secure' => true,
    // one of "Lax"|"Strict"|"None"
    'same_site' => 'Lax',
    // A session with inactivity greater than the idle_timeout will be regenerated and flushed
    'idle_timeout_in_sec' => 60 * 15,
    // Rotate session ids periodically
    'rotation_interval_in_sec' => 60 * 10,
    // Setting this value to NULL will make the session a "browser session".
    // Setting this to any positive integer will mean that the session will be regenerated and flushed
    // independently of activity.
    'absolute_lifetime_in_sec' => null,
    // The percentage that any given call to SessionManager::gc() will trigger garbage collection
    // of inactive sessions.
    'garbage_collection_percentage' => 2,
]);

创建序列化器

此包附带两个内置序列化器

  1. JsonSerializer,它假定您的所有会话内容都是 JsonSerializable 或等效的。
  2. PHPSerializer,它将使用 serializeunserialize

如果这些都不起作用,您只需实现 Serializer 接口

创建会话驱动器

SessionDriver 是一个 接口,它抽象化了会话数据的具体存储后端。

目前,以下驱动器可用

  • InMemoryDriver,用于测试期间的使用。

  • EncryptedDriver,接受一个SessionDriver作为参数,并对其数据进行加密/解密。

  • Psr16Driver,允许您使用任何PSR-16缓存。您可以通过使用snicco/session-psr16-bridge来使用此驱动程序。

  • WPDBDriver,您可以使用snicco/session-wp-bridge通过WordPress数据库存储会话。

  • WP_Object_Cache,您可以使用snicco/session-wp-bridge通过WordPress对象缓存存储会话。

  • Custom,如果您上述任何驱动程序都不适用(并且没有PSR-16适配器),您可以使用snicco/session-testing来测试您的自定义实现与接口。

创建会话管理器

SessionManager负责创建和持久化Session对象。

use Snicco\Component\Session\SessionManager\SessionManger;

$configuration = /* */
$serializer = /* */
$driver = /* */

$session_manger = new SessionManger($configuration, $driver, $serializer);

启动会话

SessionManager使用CookiePool的实例来启动会话。

您可以从$_COOKIE超级全局变量或任何普通的array实例化此对象。

调用SessionManger::start()将处理

  1. 如果提供的id在驱动程序中找不到(或不存在),则拒绝会话id并生成一个新的空会话。
  2. 根据您的配置旋转会话id。
  3. 根据您的配置旋转和清除空闲的会话。
use Snicco\Component\Session\SessionManager\SessionManger;
use Snicco\Component\Session\ValueObject\CookiePool;

$configuration = /* */
$serializer = /* */
$driver = /* */

$session_manger = new SessionManger($configuration, $driver, $serializer);

// using $_COOKIE
$cookie_pool = CookiePool::fromSuperGlobals();

// or any array.
$cookie_pool = new CookiePool($psr7_request->getCookieParams());


$session = $session_manger->start($cookie_pool);

调用SessionManager::start()将返回Session的实例。 Session是一个接口,它扩展了MutableSession接口ImmutableSession接口

这允许您清楚地分离对会话的读取和写入的不同关注点。

在您的代码中,您应该依赖于MutableSessionImmutableSession

Session接口仅用于通过会话管理器持久化会话。

不可变会话

ImmutableSession仅包含返回数据的return方法。无法修改会话。

use Snicco\Component\Session\ImmutableSession;
use Snicco\Component\Session\Session;
use Snicco\Component\Session\ValueObject\ReadOnlySession;

/**
* @var Session $session 
*/
$session = $session_manger->start($cookie_pool);

// You can either rely on type-hints or transform $session to an immutable object like so:
$read_only_session = ReadOnlySession::fromSession($session);

function readFromSession(ImmutableSession $session) {
    
    $session->id(); // instance of SessionId
    
    $session->isNew(); // true/false
        
    $session->userId(); // int|string|null
        
    $session->createdAt(); // timestamp. Can never be changed.
        
    $session->lastRotation(); // timestamp
    
    $session->lastActivity(); // last activity is updated each time a session is saved.
    
    $session->has('foo'); // true/false
    
    $session->boolean('wants_beta_features'); // true/false
    
    $session->only(['foo', 'bar']); // only get keys "foo" and "bar"
    
    $session->get('foo', 'default'); // get key "foo" with optional default value
    
    $session->all(); // Returns array of all user provided data.
    
    $session->oldInput('username', ''); // Old input is flushed after saving a session twice.
    
    $session->hasOldInput('username'); // true/false
    
    $session->missing(['foo', 'bar']); // Returns true if all the given keys are not in the session.
    
    $session->missing(['foo', 'bar']); // Returns true if all the given keys are in the session.
    
}

可变会话

Mutable仅包含修改数据的modify方法。无法读取会话数据。

use Snicco\Component\Session\MutableSession;
use Snicco\Component\Session\Session;
use Snicco\Component\Session\ValueObject\ReadOnlySession;

/**
* @var Session $session 
*/
$session = $session_manger->start($cookie_pool);

function modifySession(MutableSession $session) {
    
    // Store the current user after authentication.
    $session->setUserId('user-1');
    // can be int|string
    $session->setUserId(1);
    
    // Rotates the session id and flushes all data.
    $session->invalidate();
        
    // Rotates the session id WITHOUT flushing data.
    $session->rotate(); 
        
    $session->put('foo', 'bar');
    $session->put(['foo' => 'bar', 'baz' => 'biz']);
        
    $session->putIfMissing('foo', 'bar');
    
    $session->increment('views');
    $session->increment('views', 2); // Increment by 2
    
    $session->decrement('views');
    $session->decrement('views', 2); // Decrement by 2
    
    $session->push('viewed_pages', 'foo-page'); // Push a value onto an array.
    
    $session->remove('foo');
    
    $session->flash('account_created', 'Your account was created'); // account_created is only available during the current request and the next request.
    
    $session->flashNow('account_created', 'Your account was created' ); // account_created is only available during the current request.
    
    $session->flashInput('login_form.email', 'calvin@snicco.io'); // This value is available during the current request and the next request.

    $session->reflash(); // Reflash all flash data for one more request.
    
    $session->keep(['account_created']); // Keep account created for one more request.
    
    $session->flush(); // Empty the session data.
    
}

访问嵌套数据

可以使用"点"访问嵌套数据。

$session->put([
    'foo' => [
        'bar' => 'baz'
    ]   
]);

var_dump($session->get('foo.bar')); // baz

闪存消息 / 旧输入

将数据闪存到会话中意味着仅在会话保存两次后才存储它。

此用法最常见的场景是在POST请求后显示通知。

// POST request: 

// create user account and redirect to success page.

$session->flash('account_created', 'Great! Your account was created.');

// session is saved.

// GET request:

echo $session->get('account_created');

// session is saved again, account_created is now gone.

旧输入的工作方式非常相似。最常见的用例是在表单验证失败时显示提交的表单数据。

// POST request: 

$username = $_POST['username'];

// validate the request...

// Validation failed.
$session->flashInput('username', $username);

// session is saved.

// GET request:

if($session->hasOldInput('username')) {
    $username = $session->oldInput('username');
    // Use username to populate the form values again.
}

// session is saved again, username is now gone.

加密会话数据

如果您在会话中存储敏感数据,可以使用EncryptedDriver

此驱动程序将包装另一个(内部)会话驱动程序,并在将其传递给应用程序代码之前加密/解密您的数据。

要正常工作,EncryptedDriver需要一个SessionEncryptor实例,这是一个没有实现且极其简单的接口。

以下是使用defuse/php-encryption加密会话的示例。

use Snicco\Component\Session\Driver\EncryptedDriver;
use Snicco\Component\Session\SessionEncryptor;

final class DefuseSessionEncryptor implements SessionEncryptor
{
    private string $key;

    public function __construct(string $key)
    {
        $this->$key = $key;
    }

    public function encrypt(string $data): string
    {
        return Defuse\Crypto\Crypto::encrypt($data, $this->key);
    }

    public function decrypt(string $data): string
    {
       return Defuse\Crypto\Crypto::decrypt($data, $this->key);
    }
}

$driver = new EncryptedDriver(
    $inner_driver,
    new DefuseSessionEncryptor($your_key)
)

保存会话

Session是一个值对象。只有在会话管理器将其保存时,会话中的更改才会被持久化。

一旦会话被保存,它就会被锁定。在对已锁定的会话调用任何状态更改方法时,将抛出SessionIsLocked异常。

对未修改的会话调用save方法,将仅使用SessionDriver::touch()更新会话的最后活动时间。

这可以消除由重叠的GET/POST请求引起的许多竞争条件,这些请求读取和写入会话。

use Snicco\Component\Session\SessionManager\SessionManger;
use Snicco\Component\Session\ValueObject\CookiePool;

$configuration = /* */
$serializer = /* */
$driver = /* */
$cookie_pool = /* */;

$session_manger = new SessionManger($configuration, $driver, $serializer);

$session = $session_manger->start($cookie_pool);

$session->put('foo', 'bar');

$session_manger->save($session);

// This will throw an exception.
$session->put('foo', 'baz');

设置会话cookie

设置cookie超出了这个库的作用范围(因为我们不知道你在应用程序中如何处理HTTP问题)。

相反,会话管理器提供了一个方法,可以从会话中检索一个SessionCookie值对象。

以下是如何使用SessionCookie类来使用setcookie设置会话cookie的示例。如果你使用的是**PSR-7**请求,你可以做类似的事情。

use Snicco\Component\Session\SessionManager\SessionManger;
use Snicco\Component\Session\ValueObject\CookiePool;

$configuration = /* */
$serializer = /* */
$driver = /* */
$cookie_pool = /* */;

$session_manger = new SessionManger($configuration, $driver, $serializer);

$session = $session_manger->start($cookie_pool);

$session->put('foo', 'bar');

$session_manger->save($session);

$cookie = $session_manger->toCookie($session);

$same_site = $cookie->sameSite();
$same_site = ('None; Secure' === $same_site) ? 'None' : $same_site;

setcookie($cookie->name(), $cookie->value(), [
    'expires' => $cookie->expiryTimestamp(),
    'samesite' => $same_site,
    'secure' => $cookie->secureOnly(),
    'path' => $cookie->path(),
    'httponly' => $cookie->httpOnly(),
]);

基于用户ID管理会话

将用户ID存储在会话中不是强制性的。

然而,如果你选择这样做,这个包提供了一些管理基于用户ID的会话的出色工具。

UserSessionsDriver扩展了SessionDriver接口。

并不是所有驱动程序都支持这个接口。

use Snicco\Component\Session\Driver\InMemoryDriver;

// The in memory driver implements UserSessionDriver
$in_memory_driver = new InMemoryDriver();

// Destroy all sessions, for all users.
$in_memory_driver->destroyAllForAllUsers();

// Destroys all sessions where the user id has been set to (int) 12.
// Useful for "log me out everywhere" functionality.
$in_memory_driver->destroyAllForUserId(12);

$session_selector = $session->id()->selector();
// Destroys all sessions for user 12 expect the passed one.
// Useful for "log me out everywhere else" functionality.
$in_memory_driver->destroyAllForUserIdExcept($session_selector, 12);

// Returns an array of SerializedSessions for user 12.
$in_memory_driver->getAllForUserId(12);

垃圾回收

你应该在每次使用会话的请求中调用SessionManager::gc()

// That's it, this will remove all idle sessions with the percentage that you configured.
$session_manager->gc();

贡献

这个存储库是Snicco项目的开发存储库的只读分支。

以下是您可以如何贡献.

报告问题和发送拉取请求

请在Snicco单一代码库中报告问题。

安全性

如果您发现一个安全漏洞,请遵循我们的披露程序