uma / psr7-hmac-bundle
PSR7Hmac 和 Symfony Security 组件之间的干净集成
Requires
- php: >=5.6.0
- symfony/config: ^2.7|^3.0
- symfony/dependency-injection: ^2.7|^3.0
- symfony/http-kernel: ^2.7|^3.0
- symfony/psr-http-message-bridge: ^1.0
- symfony/security-bundle: ^2.7|^3.0
- uma/psr7-hmac: 0.6.0
- zendframework/zend-diactoros: ^1.3
Requires (Dev)
- doctrine/annotations: ^1.4
- guzzlehttp/guzzle: ^6.2
- symfony/framework-bundle: ^2.7|^3.0
- symfony/yaml: ^2.7|^3.0
README
将 Psr7Hmac 认证库与 Symfony 安全组件(Security Component)之间进行干净集成。
服务器设置
将 uma/psr7-hmac-bundle
添加到您的 composer.json
文件
php composer.phar require "uma/psr7-hmac-bundle"
并在 app/AppKernel.php
中注册该包
public function registerBundles() { return [ // ... new UMA\Psr7HmacBundle\UMAPsr7HmacBundle(), ]; }
创建您自己的 User
实体并实现该包提供的 HmacApiClientInterface
。或者,您可以实现方便的 HmacApiUserInterface
,它将 Symfony 的 UserInterface
和 HmacApiClientInterface
合并在一起。
use Symfony\Component\Security\Core\User\AdvancedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; use UMA\Psr7HmacBundle\Definition\HmacApiClientInterface; use UMA\Psr7HmacBundle\Definition\HmacApiUserInterface; class User implements UserInterface, HmacApiClientInterface { // mandatory method declarations... } class User implements HmacApiUserInterface // Equivalent to the above class definition { } class User implements AdvancedUserInterface, HmacApiClientInterface // This would be acceptable, too { }
Symfony 文档提供了关于如何实现 UserInterface
的许多指导(请参阅此处)。要实现 HmacApiClientInterface
,您可以将实体基于以下骨架,或查看展示项目中的完整示例(请参阅此处)。
namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; use UMA\Psr7HmacBundle\Definition\HmacApiClientInterface; /** * @ORM\Entity() * @ORM\Table(name="api_users") */ class ApiUser implements UserInterface, HmacApiClientInterface { // A 18 byte sequence produces a 24 character long // base64 string with no padding (trailing equal signs). // Moreover, 18 bytes equal 144 bits which is even more than // what a standard UUID has (128 bits of entropy). const BYTES_OF_ENTROPY = 18; // other constants and attribute declarations... /** * @var string * * @example 'MDEyMzQ1Njc4OUFCQ0RFRkdI' * * @ORM\Column(name="api_key", type="string", length=24, unique=true) */ private $apiKey; /** * @var string * * @example 'MDEyMzQ1Njc4OUFCQ0RFRkdI' * * @ORM\Column(name="shared_secret", type="string", length=24) */ private $sharedSecret; public function __construct() { $this->apiKey = base64_encode(random_bytes(static::BYTES_OF_ENTROPY)); $this->sharedSecret = base64_encode(random_bytes(static::BYTES_OF_ENTROPY)); } /** * {@inheritdoc} */ public function getApiKey() { return $this->apiKey; } /** * {@inheritdoc} */ public function getSharedSecret() { return $this->sharedSecret; } // other method declarations... }
最后,在您的 app/config/security.yml
文件中配置一个 hmac
防火墙。
security: providers: # you can define your user provider in any other way you want, just make sure that the # users' class implements both the UserInterface and the HmacApiClientInterface provider.api_user: entity: class: AppBundle\Entity\ApiUser property: apiKey firewalls: dev: pattern: ^/(_(profiler|wdt|homepage)|css|images|js)/ security: false api: pattern: ^/ hmac: ~ provider: provider.api_user # this flag is mandatory because persistent sessions do not make # any sense in the context of HMAC authentication stateless: true # The UMAPsr7HmacBundle is only concerned about user authentication (who are they). Therefore you must # handle their authorization (what they can do) using Symfony's standard mechanisms, such as roles and voters. access_control: - { path: ^/, roles: ROLE_API_USER }
此时,您可能想持久化 ApiUser 实体以开始发送第一个请求,因此请确保您的防火墙下至少有一个受保护的路径。
客户端设置
为了发送 HMAC 认证的请求,客户端需要安装 Psr7Hmac 库(通过 Composer 提供)并持有来自应用用户之一的 API 密钥和共享密钥。HMAC 请求的生命周期如下
客户端
- 客户端根据需要准备一个 PSR-7 的 RequestInterface 实例。Guzzle 请求对象是此类实现之一,但 Psr7Hmac 支持许多其他实现。
- 然后,它设置一个包含 API 密钥的
Api-Key
HTTP 头。 - 使用共享密钥实例化一个新的 Psr7Hmac 的
Signer
类,并将步骤 2 中的请求传递给其sign
方法,该方法生成一个签名请求。 - 将签名请求发送到服务器,例如使用 Guzzle 的客户端服务。
服务器
- 服务器接收到请求后,提取其
Api-Key
头并查找具有匹配密钥的 ApiUser。 - 如果找到,则从数据库检索用户并尝试使用他的共享密钥生成与请求中存在的相同的签名。
- 如果签名匹配,则认证成功,请求将传递到控制器,并且相关的 API 用户将通过
Controller::getUser()
帮助器或 Symfony 的TokenStorage
服务可用。 - 如果上述 3 个步骤中的任何一个失败,该包将为应用程序安排 401 未授权响应,并且控制器永远不会到达。
简短客户端示例
本例假设
- 服务器(一个Symfony应用程序)已正确设置bundle,如上所述。
- 它有一个持久的ApiUser测试用例,可以从中获取客户端的API密钥和共享密钥。
- 服务器定义了一个类似于旧版Symfony标准版的DemoBundle中包含的
/hello/{name}
。
// client.php require_once __DIR__.'/vendor/autoload.php'; $client = new \GuzzleHttp\Client(); $signer = new \UMA\Psr7Hmac\Signer('am9qZHNhOGo4ZGE4c2o4OGRo'); // It is very important to provide the full URI with the domain name because // the Host HTTP header is used by the signature algorithm. // On a development environment https://#/hello/turtle would also work. $request = (new \GuzzleHttp\Psr7\Request('GET', 'https://example.com/hello/turtle')) ->withHeader('Api-Key', 'MDEyMzQ1Njc4OUFCQ0RFRkdI'); $signedRequest = $signer->sign($request); // Hint: modify the $signedRequest here $response = $client->send($signedRequest); var_dump((string) $response->getBody()); // Hello turtle!
如果您在将其发送到服务器之前修改了$signedRequest,它将返回HTTP 401响应。
高级客户端示例
UMAPsr7HmacBundle试图解决的是PHP后端通过HTTP通信时的HMAC身份验证。在这种情况下,客户端和服务器可能都是Symfony项目,而ApiUsers实际上代表了消耗服务器通过HTTP提供的某种服务的应用程序。
如果确实是这样,我建议在客户端利用DIC做一些工作,并构建一个无差别的“SDK服务”,该服务接受纯PSR-7请求,将其标记为Api-Key头,签名并发送它们,然后返回服务器响应。
这将是总体思路
// app/config/parameters.yml example.api_key: 'MDEyMzQ1Njc4OUFCQ0RFRkdI' example.shared_secret: 'am9qZHNhOGo4ZGE4c2o4OGRo'
// app/config/services.yml services: example_hmac_signer: class: UMA\Psr7Hmac\Signer arguments: ["%example.shared_secret%"] example_sdk: class: AppBundle\Service\ExampleSDK arguments: ["%example.api_key%", "@example_hmac_signer"]
// src/AppBundle/Service/ExampleSDK.php namespace AppBundle\Service; use UMA\Psr7Hmac\Signer; class ExampleSDK { private $apiKey; private $signer; public function __construct(string $apiKey, Signer $signer) { $this->signer = $signer; $this->apiKey = $apiKey; } public function send(RequestInterface $request): ResponseInterface { // some sort of adaptation of the "brief client example" } }
高级配置
UMAPsr7HmacBundle有几个旋钮可以进一步自定义。
自定义Api-Key
头
在security.yml
文件中的hmac
键下设置apikey_header
。砰,搞定。
// app/config/security.yml firewalls: default: pattern: ^/api hmac: apikey_header: 'X-My-Custom-Header-Key' stateless: true
自定义错误响应
默认情况下,该bundle将为在hmac防火墙内部发生的每个身份验证错误返回简短的401 Unauthorized
响应。
然而,Symfony的安全组件有一个不太为人所知的特性,称为入口点,它们只是AuthenticationEntryPointInterface的实现,并允许您做到这一点。
// app/config/security.yml firewalls: default: pattern: ^/api hmac: ~ stateless: true entry_point: my_entry_point_id
常见问题解答(FAQ)
我不喜欢长随机API密钥
那么,只需在您的用户中返回您想要的任何内容,例如电子邮件或任何其他内容。只需确保这些“API密钥”在所有用户之间都是唯一的,也许甚至在数据库级别使用UNIQUE约束。
您不应该在服务器数据库中存储共享密钥的散列,就像您处理常规密码一样吗?
那听起来很棒,但您不能这么做。
您能够将用户密码作为加密散列存储的唯一原因是因为您的应用程序实际上不需要它们的内容,而是只需要检查存储的密码是否与登录尝试期间接收到的密码相同,为此您只需比较它们的散列即可。另一方面,共享密钥的内容实际上需要用于计算HMAC签名。
更积极的是,机器生成的随机共享密钥的敏感性远不如在互联网上可能被重用的用户提供的密码,因此对它们进行散列的额外麻烦甚至可能没有必要。