jasny / http-signature
IETF HTTP Signatures 草案 RFC 的实现
Requires
- php: >=7.2.0
- improved/improved: ^0.1.0
- nesbot/carbon: ^2.12
- psr/http-factory: ^1.0
- psr/http-message: ^1.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- guzzlehttp/guzzle: ^6.3
- jasny/php-code-quality: ^2.4
- php-http/client-common: ^2.0
- php-http/mock-client: ^1.3
README
此库提供了一种实现 IETF HTTP Signatures 草案 RFC 的服务。它包括用于签名请求(通过 Guzzle 等HTTP 客户端)和验证 http 签名的 PSR-7 兼容中间件。
安装
composer require jasny/http-signature
使用方法
创建 HttpSignature
服务时,传递一个支持算法列表、签名请求的回调和验证签名的回调。
use Jasny\HttpSignature\HttpSignature; $keys = [ 'hmac-key-1' => 'secret', 'hmac-key-2' => 'god', ]; $service = new HttpSignature( 'hmac-sha256', function (string $message, string $keyId) use ($keys): string { if (!isset($keys[$keyId])) { throw new OutOfBoundsException("Unknown sign key '$keyId'"); } $key = $keys[$keyId]; return hash_hmac('sha256', $message, $key, true); }, function (string $message, string $signature, string $keyId) use ($keys): bool { if (!isset($keys[$keyId])) { return false; } $key = $keys[$keyId]; $expected = hash_hmac('sha256', $message, $key, true); return hash_equals($expected, $signature); } );
签名请求
您可以使用此服务对 PSR-7 请求进行签名。
$request = new Request(); // Any PSR-7 compatible Request object $signedRequest = $service->sign($request, $keyId);
验证请求
您可以使用此服务验证已签名的 PSR-7 请求的签名。
$request = new Request(); // Any PSR-7 compatible Request object $service->verify($request);
如果请求未签名、签名无效或请求不符合要求,将抛出 HttpSignatureException
。
配置服务
多种算法
在构造函数中,可以指定一个支持算法的数组,而不是指定单个算法。使用的算法作为额外参数传递给签名和验证回调。
use Jasny\HttpSignature\HttpSignature; $service = new HttpSignature( ['hmac-sha256', 'rsa', 'rsa-sha256'], function (string $message, string $keyId, string $algorithm): string { // ... }, function (string $message, string $signature, string $keyId, string $algorithm): bool { // ... } );
签名时指定算法;
$signedRequest = $service->sign($request, $keyId, 'hmac-sha256');
或者您可以选择包含一种算法的服务副本。
$signService = $service->withAlgorithm('hmac-sha256'); $signService->sign($request, $keyId);
必需的头部
默认情况下,请求目标(包括 HTTP 方法、URL 路径和查询参数)和 Date
头部对于所有类型的请求都必须包含在签名消息中。
$service = $service->withRequiredHeaders('POST', ['(request-target)', 'date', 'content-type', 'digest']);
必需的头部可以针对每种请求方法或作为 default
指定。
请注意,要求仅适用于将头部包含在创建签名的过程中。如果头部未用于请求,它们也不属于签名的一部分。检查请求中是否设置了头部并具有有效的值,不属于此库的范围。
Date
头部
如果指定了 Date
头部,服务将检查请求的年龄。如果签名过早,将抛出异常。默认情况下,请求不能超过 300 秒(5 分钟)。
请求签名和验证之间的时间可能由延迟或客户端和/或服务器的系统时钟偏移造成。
允许的时间可以配置为时钟偏移;
$service = $service->withClockSkew(1800); // Accept requests up to 30 minutes old
X-Date
头部
浏览器自动为 AJAX 请求设置 Date
头部。这使得无法使用它进行签名。作为解决方案,可以使用覆盖 Date
头部的 X-Date
头部。
服务器中间件
可以使用服务器中间件验证 PSR-7 请求。
如果请求已签名但签名无效,中间件将返回 401 Unauthorized
响应,并且不会调用处理器。
单次传递中间件(PSR-15)
中间件实现了 PSR-15 的 MiddlewareInterface
。由于 PSR 标准,许多新的库支持此类中间件,例如 Zend Stratigility。
您需要提供 PSR-17 响应工厂,以创建对具有无效签名的请求返回 401 Unauthorized
响应。
use Jasny\HttpSignature\HttpSignature; use Jasny\HttpSignature\ServerMiddleware; use Zend\Stratigility\MiddlewarePipe; use Zend\Diactoros\ResponseFactory; $service = new HttpSignature(/* ... */); $responseFactory = new ResponseFactory(); $middleware = new ServerMiddleware($service, $responseFactory); $app = new MiddlewarePipe(); $app->pipe($middleware);
双次传递中间件
我的PHP库支持双遍历中间件。这些中间件具有以下签名:
fn(ServerRequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface
为了将回调用于Jasny Router和Relay库,请使用asDoublePass()
方法。
当使用双遍历中间件时,提供资源工厂是可选的。如果没有提供,它将使用调用时传递的响应。
use Jasny\HttpSignature\HttpSignature; use Jasny\HttpSignature\ServerMiddleware; use Relay\RelayBuilder; $service = new HttpSignature(/* ... */); $middleware = new ServerMiddleware($service); $relayBuilder = new RelayBuilder($resolver); $relay = $relayBuilder->newInstance([ $middleware->asDoublePass(), ]); $response = $relay($request, $baseResponse);
验证请求
如果请求已签名且签名有效,中间件将设置signature_key_id
请求属性。
对于未签名的请求,中间件不执行任何操作。这意味着您需要始终检查请求是否具有signature_key_id
。
$keyId = $request->getAttribute(`signature_key_id`); if ($keyId === null) { $errorResponse = $response ->withStatus(401) ->withHeader('Content-Type', 'text/plain'); $errorResponse = $service->setAuthenticateResponseHeader($errorResponse); $errorResponse->getBody()->write('request not signed'); } // Request is signed and signature is valid // ...
客户端中间件
客户端中间件可用于对通过PSR-7兼容的HTTP客户端(如Guzzle和HTTPlug)发送的请求进行签名。
use Jasny\HttpSignature\HttpSignature; use Jasny\HttpSignature\ClientMiddleware; $service = new HttpSignature(/* ... */); $middleware = new ClientMiddleware($service, $keyId);
使用$keyId
来设置Authorization
头部,并将其传递给签名回调。
如果服务支持多种算法,您需要使用withAlgorithm
方法选择一个。
$middleware = new ClientMiddleware($service->withAlgorithm('hmac-sha256'));
双次传递中间件
客户端中间件可用于任何支持双遍历中间件的客户端。此类中间件具有以下签名:
fn(RequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface
大多数HTTP客户端不支持双遍历中间件,而是使用单遍历。然而,更通用的PSR-7中间件库,如Relay,则支持双遍历。
use Relay\RelayBuilder; $relayBuilder = new RelayBuilder($resolver); $relay = $relayBuilder->newInstance([ $middleware->asDoublePass(), ]); $response = $relay($request, $baseResponse);
客户端中间件不符合PSR-15(单遍历),因为它是为服务器请求而设计的。
Guzzle
Guzzle是PHP最受欢迎的HTTP客户端。中间件有一个forGuzzle()
方法,该方法创建一个可以作为Guzzle中间件使用的回调。
使用中间件为Guzzle时,不需要将$keyId
传递给构造函数。而是使用Guzzle选项signature_key_id
。这也允许选项按请求使用不同的密钥或禁用请求的签名。
use GuzzleHttp\HandlerStack; use GuzzleHttp\Client; use Jasny\HttpSignature\HttpSignature; use Jasny\HttpSignature\ClientMiddleware; $service = new HttpSignature(/* ... */); $middleware = new ClientMiddleware($service); $stack = new HandlerStack(); $stack->push($middleware->forGuzzle()); $client = new Client(['handler' => $stack, 'signature_key_id' => $keyId]); $client->get('/foo'); // Sign with default key $client->get('/foo', ['signature_key_id' => $altKeyId]); // Sign with other key $client->get('/foo', ['signature_key_id' => null]); // Don't sign
或者,您可以选择默认禁用签名,仅在指定时签名;
$client = new Client(['handler' => $stack]); $client->get('/foo'); // Don't sign $client->get('/foo', ['signature_key_id' => $keyId]); // Sign
选项仅适用于Guzzle。对于HTTPlug和其他客户端,您需要为每个密钥创建一个客户端或使用中间件进行签名。
HTTPlug
HTTPlug是PHP-HTTP的HTTP客户端。它允许您编写需要HTTP客户端的可重用库和应用程序,而无需绑定到特定实现。
中间件的forHttplug()
方法创建一个可以用于HTTPlug插件的对象。
use Http\Discovery\HttpClientDiscovery; use Http\Client\Common\PluginClient; use Jasny\HttpSignature\HttpSignature; use Jasny\HttpSignature\ClientMiddleware; $service = new HttpSignature(/* ... */); $middleware = new ClientMiddleware($service, $keyId); $pluginClient = new PluginClient( HttpClientDiscovery::find(), [ $middleware->forHttplug(), ] );