kamermans/guzzle-oauth2-subscriber

Guzzle 4, 5, 6 和 7+ 的 OAuth 2.0 客户端

v1.1.0 2024-05-02 18:45 UTC

README

已在 Guzzle 4, 5, 6, 7 和 PHP 7.1, 7.2, 7.3, 7.4, 8.0 和 8.1 上测试。

这是一个 Guzzle 的 OAuth 2.0 客户端,旨在与 Guzzle 4, 5, 6, 7 和同一包内的所有未来版本保持 100% 兼容。尽管我喜欢 Guzzle,但其接口始终在变化,大约每 12 个月就会造成大规模的破坏性更改,因此我创建了此包以帮助减少大多数第三方 Guzzle 依赖带来的依赖地狱。我写了官方的 Guzzle OAuth 2.0 插件,它仍然位于 oauth2 分支上,在官方 Guzzle 仓库,但我看到他们已经停止了对 master 上的 Guzzle < v6 的支持,这促使我将这个分支拆分出来成为一个单独的包。

功能

  • 通过支持的授权类型之一(代码、客户端凭证、用户凭证、刷新令牌)获取访问令牌。或者你可以自己设置访问令牌。
  • 支持刷新令牌(存储它们并使用它们获取新的访问令牌)。
  • 处理令牌过期(获取新的令牌并重试失败的请求)。
  • 允许通过回调存储和查找访问令牌

安装

可以使用 Composer 安装此项目。运行 composer require kamermans/guzzle-oauth2-subscriber 或将以下内容添加到您的 composer.json

    {
        "require": {
            "kamermans/guzzle-oauth2-subscriber": "~1.1"
        }
    }

用法

此插件与 Guzzle 无缝集成,透明地添加身份验证到出站请求,并且如果访问令牌已不再有效,可选地尝试重新授权。

有多个授权类型可供选择,如 PasswordCredentialsClientCredentialsAuthorizationCode

Guzzle 4 & 5 与 Guzzle 6+

Guzzle 6 的发布中,大部分库都被重构或完全重写,因此此库的集成方式不同。

发射器(Guzzle 4 & 5)

Guzzle 4 & 5 使用 事件订阅者,此库包含用于此目的的 OAuth2Subscriber

$oauth = new OAuth2Subscriber($grant_type);

$client = new Client([
    'auth' => 'oauth',
]);

$client->getEmitter()->attach($oauth);

中间件(Guzzle 6+)

从 Guzzle 6 开始,使用 中间件 来集成 OAuth,此库包含用于此目的的 OAuth2Middleware

$oauth = new OAuth2Middleware($grant_type);

$stack = HandlerStack::create();
$stack->push($oauth);

$client = new Client([
    'auth'     => 'oauth',
    'handler'  => $stack,
]);

或者,您可以将中间件添加到现有的 Guzzle 客户端

$oauth = new OAuth2Middleware($grant_type);
$client->getConfig('handler')->push($oauth);

客户端凭证示例

客户端凭证通常用于服务器到服务器的身份验证。使用此授权类型,客户端在其自己的 behalf 请求授权,因此只有两方参与。至少需要一个 client_idclient_secret,尽管许多服务还需要一个 scope 和其他参数。

以下是 Guzzle 4 和 Guzzle 5 中客户端凭证方法的示例

use kamermans\OAuth2\GrantType\ClientCredentials;
use kamermans\OAuth2\OAuth2Subscriber;

// Authorization client - this is used to request OAuth access tokens
$reauth_client = new GuzzleHttp\Client([
    // URL for access_token request
    'base_url' => 'http://some_host/access_token_request_url',
]);
$reauth_config = [
    "client_id" => "your client id",
    "client_secret" => "your client secret",
    "scope" => "your scope(s)", // optional
    "state" => time(), // optional
];
$grant_type = new ClientCredentials($reauth_client, $reauth_config);
$oauth = new OAuth2Subscriber($grant_type);

// This is the normal Guzzle client that you use in your application
$client = new GuzzleHttp\Client([
    'auth' => 'oauth',
]);
$client->getEmitter()->attach($oauth);
$response = $client->get('http://somehost/some_secure_url');

echo "Status: ".$response->getStatusCode()."\n";

以下是相同示例的 Guzzle 6+

use kamermans\OAuth2\GrantType\ClientCredentials;
use kamermans\OAuth2\OAuth2Middleware;
use GuzzleHttp\HandlerStack;

// Authorization client - this is used to request OAuth access tokens
$reauth_client = new GuzzleHttp\Client([
    // URL for access_token request
    'base_uri' => 'http://some_host/access_token_request_url',
]);
$reauth_config = [
    "client_id" => "your client id",
    "client_secret" => "your client secret",
    "scope" => "your scope(s)", // optional
    "state" => time(), // optional
];
$grant_type = new ClientCredentials($reauth_client, $reauth_config);
$oauth = new OAuth2Middleware($grant_type);

$stack = HandlerStack::create();
$stack->push($oauth);

// This is the normal Guzzle client that you use in your application
$client = new GuzzleHttp\Client([
    'handler' => $stack,
    'auth'    => 'oauth',
]);

$response = $client->get('http://somehost/some_secure_url');

echo "Status: ".$response->getStatusCode()."\n";

授权代码示例

examples/ 目录中有一个使用 AuthorizationCode 授权类型和 RefreshToken 的完整示例。

授权类型

以下 OAuth 授权类型直接支持,您始终可以通过实现 kamermans\OAuth2\GrantType\GrantTypeInterface 来创建自己的。

  • AuthorizationCode
  • ClientCredentials
  • PasswordCredentials
  • RefreshToken

这些中的每一个都将 Guzzle 客户端作为第一个参数。此客户端用于获取或刷新您的 OAuth 访问令牌,从您正在发出的其他请求中独立出来。

请求签名者

有两种情况下我们需要在HTTP请求中签名:在向请求添加客户端凭据以获取新的访问令牌时,以及在请求中添加访问令牌时。

客户端凭据签名者

在请求新的访问令牌时,我们需要将所需的凭据发送到OAuth 2服务器。在请求中添加信息在本库中称为签名

kamermans\OAuth2\Signer\ClientCredentials 包含两个客户端凭据签名者

  • BasicAuth:(默认) 使用HTTP Basic Auth在Authorization头中发送凭据到OAuth 2服务器。
  • PostFormData:使用HTTP表单正文(Content-Type: application/x-www-form-urlencoded)将凭据发送到OAuth 2服务器。客户端ID存储在client_id字段中,客户端密钥存储在client_secret字段中。可以通过构造函数传递参数来更改字段名,例如:new PostFormData('MyClientId', 'MySecret');(这将ID和密钥放入MyClientIdMySecret字段)。
  • Json:使用JSON(Content-Type: application/json)将凭据发送到OAuth 2服务器。客户端ID存储在client_id字段中,客户端密钥存储在client_secret字段中。可以通过构造函数传递参数来更改字段名,例如:new Json('MyClientId', 'MySecret');(这将ID和密钥放入MyClientIdMySecret字段)。

如果你从的OAuth 2服务器不支持内置方法,你可以扩展内置签名者之一,或者通过实现kamermans\OAuth2\Signer\ClientCredentials\SignerInterface来创建自己的,例如

use kamermans\OAuth2\Signer\ClientCredentials\SignerInterface;

class MyCustomAuth implements SignerInterface
{
    public function sign($request, $clientId, $clientSecret)
    {
        if (Helper::guzzleIs('~', 6)) {
            $request = $request->withHeader('x-client-id', $clientId);
            $request = $request->withHeader('x-client-secret', $clientSecret);
            return $request;
        }

        $request->setHeader('x-client-id', $clientId);
        $request->setHeader('x-client-secret', $clientSecret);
        return $request;
    }
}

访问令牌签名者

当向由OAuth 2保护的REST端点发出请求时,我们需要通过将访问令牌添加到请求中来签名请求。此库拦截您的请求,使用当前访问令牌对其进行签名,并发送它们。

kamermans\OAuth2\Signer\AccessToken中包括两种最常见的签名请求的方式

  • BearerAuth:(默认) 使用HTTP Authorization头发送访问令牌。
  • BasicAuthBearerAuth的别名。不要使用;仅用于向后兼容。
  • QueryString:通过将其附加到查询字符串来发送访问令牌。默认查询字符串字段名为access_token,如果该字段已在请求中存在,则将其覆盖。可以通过传递给构造函数来使用不同的字段名,例如:new QueryString('MyAccessToken'),其中MyAccessToken是字段名。

注意:不建议使用QueryString签名者,因为您的访问令牌会暴露在URL中。此外,您应仅通过HTTPS连接到OAuth服务,以便您的访问令牌在传输过程中被加密。

您可以通过实现kamermans\OAuth2\Signer\AccessToken\SignerInterface来创建自定义访问令牌签名者。

访问令牌持久性

注意:OAuth访问令牌应安全地存储并/或加密。如果攻击者获得了访问令牌,他们可能会无限制地访问任何允许的资源和服务范围!

默认情况下,访问令牌不会在任何地方持久化。有一些内置机制用于缓存/持久化令牌(在kamermans\OAuth2\Persistence中)

  • NullTokenPersistence(默认)禁用持久化
  • FileTokenPersitence接受要保存访问令牌的文件的路径。
  • DoctrineCacheTokenPersistence接受一个Doctrine\Common\Cache\Cache对象和可选的键名(默认:guzzle-oauth2-token),其中访问令牌将被保存。
  • SimpleCacheTokenPersistence接受一个PSR-16 SimpleCache和可选的键名(默认:guzzle-oauth2-token),其中访问令牌将被保存。这允许使用任何PSR-16兼容的缓存。
  • Laravel5CacheTokenPersistence 接受一个 Illuminate\Contracts\Cache\Repository 对象,并可选择一个键名(默认:guzzle-oauth2-token),用于保存访问令牌。
  • ClosureTokenPersistence 允许您通过提供闭包来处理持久化函数,定义一个令牌持久化提供者。

如果您想使用自己的持久化层,应该编写自己的类来实现 TokenPersistenceInterface 或使用本节末尾描述的 ClosureTokenPersistence 提供者。

要启用令牌持久化,您必须使用 OAuth2Middleware::setTokenPersistence()OAuth2Subscriber::setTokenPersistence() 方法,如下所示

use kamermans\OAuth2\Persistence\FileTokenPersistence;

$token_path = '/tmp/access_token.json';
$token_persistence = new FileTokenPersistence($token_path);

$grant_type = new ClientCredentials($reauth_client, $reauth_config);
$oauth = new OAuth2Middleware($grant_type);
$oauth->setTokenPersistence($token_persistence);

基于闭包的令牌持久化

有很多情况下,您可能想使用自己的缓存层来存储 OAuth2 数据,但是没有包含与您的缓存提供者一起工作的适配器。通过允许您定义处理 OAuth2 持久化数据的闭包,ClosureTokenPersistence 提供者使这种情况变得更容易,如下面的示例所示。

// We'll store everything in an array, but you can use any provider you want
$cache = [];
$cache_key = "foo";

// Returns true if the item exists in cache
$exists = function() use (&$cache, $cache_key) {
    return array_key_exists($cache_key, $cache);
};

// Sets the given $value array in cache
$set = function(array $value) use (&$cache, $cache_key) {
    $cache[$cache_key] = $value;
};

// Gets the previously-stored value from cache (or null)
$get = function() use (&$cache, $cache_key, $exists) {
    return $exists()? $cache[$cache_key]: null;
};

// Deletes the previously-stored value from cache (if exists)
$delete = function() use (&$cache, $cache_key, $exists) {
    if ($exists()) {
        unset($cache[$cache_key]);
    }
};

$persistence = new ClosureTokenPersistence($set, $get, $delete, $exists);

注意:令牌数据格式是 PHP 关联数组。您可以在存储之前使用 serialize()json_encode() 或其他您想要的任何方式来简化数组,但请记住,在返回之前在 get() 中将其解码回数组!此外,上述示例不是非常线程安全的,所以如果您有很高的并发级别,您需要找到更多原子的方法来处理这种逻辑,或者至少用 try/catch 包装东西并优雅地处理这些。

请参阅 src/Persistence/ 目录以获取有关持久化的更多信息。

手动设置访问令牌

对于手动获取的访问令牌,您可以使用 NullGrantType 并手动设置访问令牌,如下所示

use kamermans\OAuth2\GrantType\NullGrantType;

$oauth = new OAuth2Middleware(new NullGrantType);
$oauth->setAccessToken([
	// Your access token goes here
    'access_token' => 'abcdefghijklmnop',
	// You can specify 'expires_in` as well, but it doesn't make much sense in this scenario
	// You can also specify 'scope' => 'list of scopes'
]);

请注意,如果未使用 setAccessToken() 设置访问令牌,则会抛出 kamermans\OAuth2\Exception\ReauthorizationException,因为 NullGrantType 没有获取新访问令牌的方法。

使用刷新令牌

刷新令牌旨在允许服务器代表不在场的用户请求新的访问令牌。例如,如果某个虚构的应用程序 Angry Rodents 想代表用户 John Doe 在社交媒体网站 Grillbook 上发布内容,Angry Rodents 应用程序需要 Grillbook 的访问令牌。当 John Doe 首次安装此应用程序时,它会将其重定向到 Grillbook 网站,以授权 Angry Rodents 应用程序代表其发布,在此过程中,Angry Rodents 应用程序会收到一个访问令牌和一个刷新令牌。最终,访问令牌会过期,但 Angry Rodents 不能每次令牌过期时都使用原始方法(将用户重定向以请求权限),因此,它将刷新令牌发送到 GrillbookGrillbook 会返回一个新的访问令牌(可能还有一个新的刷新令牌)。

要使用刷新令牌,您需要将 RefreshToken 授予类型对象作为第二个参数传递给 OAuth2MiddlewareOAuth2Subscriber。通常,刷新令牌仅在交互式 AuthorizationCode 授予类型(用户在场)中使用,但也可以与其他授予类型一起使用(这在 OAuth 2.0 规范中是不推荐的)。例如,我们在这里使用刷新令牌与 ClientCredentials 授予类型一起使用

// This grant type is used to get a new Access Token and Refresh Token when
//  no valid Access Token or Refresh Token is available
$grant_type = new ClientCredentials($reauth_client, $reauth_config);

// This grant type is used to get a new Access Token and Refresh Token when
//  only a valid Refresh Token is available
$refresh_grant_type = new RefreshToken($reauth_client, $reauth_config);

// Tell the middleware to use the two grant types
$oauth = new OAuth2Middleware($grant_type, $refresh_grant_type);

当使用刷新令牌请求新的访问令牌时,服务器可能会在响应中发送新的刷新令牌。如果发送了新的刷新令牌,它将被保存,否则将保留旧的刷新令牌。