snicco/signed-url

一个小型、框架无关的库,用于创建和验证signed-url。

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. 创建一个signed-url
    3. 验证一个signed url
      1. PSR-15中间件
      2. 其他PHP应用
    4. 存储类型
      1. 会话
      2. 内存中
      3. PSR-16
      4. 实现自己的
  4. 贡献
  5. 问题和PR
  6. 安全性

动机

在开发Snicco项目时,我们没有找到任何好的独立PHP库用于签名url。我们需要在几个地方使用此功能,因此我们决定自己实现。

功能

  • 使用强随机秘密,由CSPRNG和安全的哈希函数生成。
  • 验证签名、过期时间和每个url的强制使用限制。
  • PSR-7/15兼容。没有PHP超级全局变量的隐藏依赖。
  • 防止基于时间的侧信道攻击。
  • 在最大使用次数后永久使signed-url无效。(旋转您的秘密将使所有signed-url无效)
  • 具有防御性编程,使得错误使用非常困难。
  • 支持多种存储后端。
  • 经过适当测试且直观的API。

虽然术语signed-url在技术上不正确(此包使用HMACs,而不是非对称签名),但我们选择坚持SymfonyLaravel的命名方式。

安装

composer require snicco/signed-url

使用

创建一个秘密

从您的项目根目录运行以下命令,并将生成的秘密存储在安全位置,该位置位于您的web根目录之外。

vendor/bin/generate-signed-url-secret

这将输出一个类似以下的随机十六进制编码的秘密:32|1e21be67f2279e485c7c5e8291d05edda7e76ffb01ddb8eb290ce826528ad2ff

此秘密绝对不应该存储在版本控制中。

在您的应用程序中,使用类似symfony/dotenv的方式从您的应用程序中加载秘密。

// require 'vendor/autoload.php';
$secret = \Snicco\Component\SignedUrl\Secret::fromHexEncoded(getenv('SIGNED_URL_SECRET'));

创建一个signed-url

$secret = /* */

$hmac = new Snicco\Component\SignedUrl\HMAC($secret, 'sha256')

// This is a simple interface.
// Use one of the inbuilt storages in the #storages section or provide your own.
$storage = /* */

$signer = new \Snicco\Component\SignedUrl\UrlSigner($storage, $hmac);

// The maximum lifetime in seconds that this link should be valid for.
$lifetime_in_sec = 60;

// The maximum amount that this link should be valid for.
// After each successfully validation this amount will be decreased by 1.
$usage_limit = 1;

// optional: adding request context that must be the same in order to
// successfully validate a signed-url.
$context = ($_SERVER['REMOTE_ADDR'] ?? '') . ($_SERVER['HTTP_USER_AGENT'] ?? '');

$signed_url = $signer->sign('https://example.com/unsubscribe?user_id=12' , $lifetime_in_sec, $usage_limit, $context);

$mailer = /* */

$href = $signed_url->asString();

// $href will be something like transformers:
// https://example.com/unsubscribe?user_id=12expires=1639783661&signature=Del1cGmLB1wVET6PJieCrQ==|1MTBBGIpEGPVuGaKDjjrHDBusMNoWB15Ng5lKBSSLQY=

$mailer->send('user12@gmail.com', "Click <a href='{{$href}}'> here <a/> to unsubscribe.")

验证signed-url

应该在中间件中执行signed-url的验证,以避免样板代码。

下面的代码示例描述了在任何PHP应用程序中验证url的手动方式。

PSR-15中间件

如果您的框架是PSR-7/PSR-15兼容的,并且支持基于路由的中间件,您可以使用我们的PSR-15中间件桥接器,这使得此操作变得非常简单。

所有PHP应用

$storage = /* */
$hmac = /* */

// Clean expired links periodically.
try {
    // 0-100
    $percentage = 2;
   \Snicco\Component\SignedUrl\GarbageCollector::clean($storage, $percentage);
   
} catch (UnavailableStorage $e) {
    // gc did not work for some reason. Log and continue.
    error_log($e->getMessage());
    
}

$validator = new \Snicco\Component\SignedUrl\SignedUrlValidator($storage, $hmac);

$target = $_SERVER['REQUEST_URI'].'?'.$_SERVER['QUERY_STRING'];

try {

    // optional context, has to be the same scheme used at creation.
    $context = ($_SERVER['REMOTE_ADDR'] ?? '') . ($_SERVER['HTTP_USER_AGENT'] ?? '');
    
    $validator->validate( $target, $context);
    
} catch (\Snicco\Component\SignedUrl\Exception\InvalidSignature $e ) {
        
   error_log("invalid signature.");     
   echo "This link has expired. Please request a new one."
    
} catch (\Snicco\Component\SignedUrl\Exception\SignedUrlExpired $e ) {

   error_log("signed url expired.");    
   echo "This link has expired. Please request a new one."
   
} catch (\Snicco\Component\SignedUrl\Exception\SignedUrlUsageExceeded $e ) {

   error_log("signed url usage exceeded.");  
   echo "This link has expired. Please request a new one."
}

// Everything is valid.
// If the link can be used multiple times the usage is decremented automatically by 1.
echo "You have been unsubscribed."

存储类型

Snicco\SignedUrl\Contracts\SingedUrlStorage为每个创建的signed-url保留一个标识符,并确保您的最大使用限制得到执行。

如果没有某种形式的后端存储,signed-url在过期时间戳之前可以无限次数地有效。(如果您想这样做,可以使用NullStorage)。

会话存储(包含)

SessionStorage 接受一个 array 或任何实现 ArrayAccess 接口的对象(通过引用传递)。

// using an array.
$storage = new \Snicco\Component\SignedUrl\Storage\SessionStorage($_SESSION);

// using an object implementing ArrayAccess
$arr = new MyArrayAccess();
$storage = new \Snicco\Component\SignedUrl\Storage\SessionStorage($arr);

NullStorage(包含)

NullStorage 不执行任何操作。不会存储签名URL,也不会实施使用限制。仅当您的签名URL在过期前应有效多次时才使用此功能。

签名URL的有效性将仅基于正确的签名和过期时间戳。

InMemory(包含)

您可以在单元测试中使用 InMemoryStorage

$storage = new \Snicco\Component\SignedUrl\Storage\InMemoryStorage()

PSR16-Cache(桥接包)

我们有一个专门的 PSR-16 桥接器,它允许您使用任何 PSR-16 缓存 作为存储。

实现自己的存储

实现自己的存储非常简单。您只需实现简单的 SingedUrlStorage 接口。

使用 snicco/signed-url-testing 测试您的实现是否符合接口的合约。

贡献

此存储库是 Snicco 项目 开发存储库的只读分割。

这就是您如何贡献.

报告问题和发送拉取请求

请在 Snicco monorepo 中报告问题。

安全性

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