quibax / php-oauth-client
PHP OAuth 2.0 客户端
Requires
- php: >=5.3.3
- ext-openssl: *
- fkooman/oauth-common: 0.5.*
- guzzle/guzzle: ~3.8
README
简介
本项目提供了一个 OAuth 2.0 "Authorization Code Grant" 客户端,该客户端基于 RFC 6749 第 4.1 节的描述。
客户端可以通过一个 PHP API 进行控制,该 API 用于从尝试访问 OAuth 2.0 受保护的资源服务器的应用程序中。
功能
以下功能被支持:
- "Authorization Code Grant" 配置文件
- 刷新令牌
许可证
根据自由软件基金会发布的 GNU 较小通用公共许可证的条款许可,许可证版本 3 或(根据您的选择)任何更高版本。
https://gnu.ac.cn/licenses/lgpl.html
这大致意味着,如果您编写了一些使用此客户端的 PHP 应用程序,您不需要将应用程序也发布在(L)GPL 下。有关确切详情,请参阅许可证。
应用程序集成
如果您想在您的应用程序中集成此 OAuth 客户端,您需要回答一些问题:
- 我将把访问令牌存储在哪里?
- 我如何在我的应用程序中创建一个端点 URL,该 URL 可以用作授权服务器回调的重定向 URL?
除了这些,您还需要来自授权服务器的 OAuth 客户端凭据以及您想要连接的服务的 REST API 文档。例如,您需要知道 authorize_endpoint、token_endpoint、client_id 和 client_secret。
关于存储访问令牌,此库包含两个后端。一个用于将令牌存储在数据库中(使用 PHP PDO 抽象层),另一个用于将它们存储在用户会话中。第一个需要一些设置,第二个非常容易使用(无需配置),但不会允许客户端在没有会话数据的情况下访问资源服务器中的数据。一个更健壮的实现会使用 PDO 支持的存储。对于测试目的或非常简单的设置,会话实现最有意义。
为了访问资源服务,有一个 Guzzle 插件可用,可以帮助您完成此操作。
以下各节将逐步介绍您需要执行的所有步骤,以便使客户端正常工作。
示例
此外,在 example 目录中提供了一个完整的示例。这包括执行令牌请求和请求数据的 index.php,还包括用于展示如何使用 Callback API 的 callback.php。此外,还包括用于与 Composer 一起使用的 composer.json。
Composer
为了轻松集成到您的应用程序中,建议使用 Composer 安装依赖项。您需要安装两个库才能使用此库:
fkooman/php-oauth-clientfkooman/guzzle-bearer-auth-plugin
以下是一个简单的 composer.json 文件示例,您可以使用它:
{
"name": "fkooman/my-demo-oauth-app",
"require": {
"fkooman/guzzle-bearer-auth-plugin": "dev-master",
"fkooman/php-oauth-client": "dev-master"
}
}
客户端配置
您可以根据以下示例创建一个客户端配置对象。如果需要,您可以从中获取应用程序中的配置文件。以下是一个 ClientConfig 类的示例:
$clientConfig = new ClientConfig(
array(
"authorize_endpoint" => "https:///oauth/php-oauth/authorize.php",
"client_id" => "foo",
"client_secret" => "foobar",
"token_endpoint" => "https:///oauth/php-oauth/token.php",
)
);
还有一个 GoogleClientConfig 类,您可以使用 Google 的 client_secrets.json 文件格式使用它。
// Google
$googleClientConfig = new GoogleClientConfig(
json_decode(file_get_contents("client_secrets.json"), true)
);
Google 类还设置了一些特定于 Google 的选项来处理一些规范违规。用于处理违反规范的服务的配置选项包括:
allow_null_expires_in在 OAuth 2.0 AS 返回"expires_in": null时,此设置在null时删除expires_in字段,从而回退到假设令牌无限期有效。将其设置为true以启用此配置选项,默认为false。与此行为相符的服务:SurveyMonkey。- 如果OAuth 2.0 AS完全省略了
token_type字段,则使用default_token_type。这允许您设置token_type。例如,如果您知道AS返回的类型是bearer,则可以将其设置为bearer。具有此行为的AS:Salesforce。 - 如果OAuth 2.0 AS在令牌端点不接受基本身份验证,则使用
credentials_in_request_body。这将强制客户端使用client_id和client_secretPOST体字段来指定凭据。此选项还将允许client_id中包含冒号(:)。具有此行为的AS:Google、SurveyMonkey、GitHub。 - 如果服务器返回的规范违反空字符串作为
scope,则使用default_server_scope。这将覆盖作用域值,允许您设置一个。它仅在服务器作用域为空字符串时设置,而不是在所有情况下作为默认设置!具有此行为的AS:Nationbuilder。 - 如果服务器要求您在刷新令牌请求中也提供redirect_uri参数,则使用
use_redirect_uri_on_refresh_token_request。具有此行为的AS:Nationbuilder。
初始化API
现在您可以初始化Api对象
$api = new Api("foo", $clientConfig, new SessionStorage(), new \Guzzle\Http\Client());
在此示例中,我们使用SessionStorage令牌存储后端。这用于在用户会话中保留获得的令牌。出于测试目的,这足够了,对于生产部署,您将希望使用PdoStorage后端,请参见下文。
您还需要提供一个Guzzle实例,Guzzle是一个HTTP客户端,用于交换授权代码以获取访问令牌,或者使用刷新令牌以获取新的访问令牌。
请求令牌
为了请求令牌,您需要使用两种方法:Api::getAccessToken()和Api::getAuthorizeUri()。第一个用于检查是否已提供令牌,第二个用于获取一个URL,您必须将浏览器从您的应用程序重定向到该URL。下面的示例将向您展示如何使用这些方法。
在您可以调用这些方法之前,您需要创建一个Context对象,以指定您为哪个用户请求此访问令牌以及您在授权服务器中请求的作用域。
$context = new Context("john.doe@example.org", new Scope("read"));
这意味着您将请求一个与john.doe@example.org绑定的令牌,作用域为read。您在此处指定的用户通常是您在您的应用程序中使用的用户标识符,该应用程序想与OAuth 2.0受保护的资源集成。在您的服务中,用户可以是john.doe@example.org。此标识符与远程服务的用户身份无关,它仅用于记录访问令牌。如果您不想请求任何特定的作用域,则可以使用new Scope()。
现在您可以检查是否已提供访问令牌
$accessToken = $api->getAccessToken($context);
如果此调用返回false,则表示此用户和作用域没有可用的访问令牌,并且无法通过使用刷新令牌的后端方式获得。这意味着从未有过令牌或它已过期。令牌仍然可以被吊销,但我们现在看不到这一点,我们将在稍后尝试使用它时发现这一点。
假设getAccessToken($context)调用返回false,即:没有令牌,我们必须获得授权
if (false === $accessToken) {
/* no valid access token available, go to authorization server */
header("HTTP/1.1 302 Found");
header("Location: " . $api->getAuthorizeUri($context));
exit;
}
如果您的应用程序没有使用任何框架,这是最简单的方法。如果您的应用程序使用框架,您可能可以使用该框架来执行“正确”的重定向,而无需自己设置HTTP头。您应该使用它!
在此之后,此脚本的流程结束,用户将被重定向到授权服务器。一旦到达那里,用户将接受客户端请求,并重定向回您在OAuth 2.0服务提供商处注册的重定向URL。您还需要在此回调位置放置一些代码,请参见下一节。
假设您已经有一个访问令牌,即:Api::getAccessToken()的响应不是false,您现在可以尝试获取资源。此示例也使用Guzzle。
$apiUrl = 'http://www.example.org/resource';
try {
$client = new Client();
$bearerAuth = new BearerAuth($accessToken->getAccessToken());
$client->addSubscriber($bearerAuth);
$response = $client->get($apiUrl)->send();
header("Content-Type: application/json");
echo $response->getBody();
} catch (BearerErrorResponseException $e) {
if ("invalid_token" === $e->getBearerReason()) {
// the token we used was invalid, possibly revoked, we throw it away
$api->deleteAccessToken($context);
$api->deleteRefreshToken($context);
/* no valid access token available, go to authorization server */
header("HTTP/1.1 302 Found");
header("Location: " . $api->getAuthorizeUri($context));
exit;
}
throw $e;
}
特别注意 BearerErrorResponseException 的情况,当访问令牌无效时,访问令牌和刷新令牌都会被删除。如果发生这种情况,浏览器将被重定向,就像还没有令牌的情况一样。
处理回调
上述情况假设你已经有一个有效的访问令牌。如果没有,你将被重定向到授权服务器,在那里你必须接受访问你数据的请求。假设一切顺利,你将被重定向回你在 OAuth 2.0 服务中注册的重定向 URI。
Callback 类的用法与 Api 类非常相似。我们假设你在这里也创建 ClientConfig 对象,就像在 Api 的情况下。这个文件的假设内容在 callback.php。
try {
$cb = new Callback("foo", $clientConfig, new SessionStorage(), new \Guzzle\Http\Client());
$cb->handleCallback($_GET);
header("HTTP/1.1 302 Found");
header("Location: http://www.example.org/index.php");
} catch (AuthorizeException $e) {
// this exception is thrown by Callback when the OAuth server returns a
// specific error message for the client, e.g.: the user did not authorize
// the request
echo sprintf("ERROR: %s, DESCRIPTION: %s", $e->getMessage(), $e->getDescription());
} catch (\Exception $e) {
// other error, these should never occur in the normal flow
echo sprintf("ERROR: %s", $e->getMessage());
}
这里需要的就是这些。授权码将被从回调 URL 中提取出来,并用于获取访问令牌。访问令牌将被存储在令牌存储中,这里为 SessionStorage,然后浏览器将被重定向回执行 Api 调用的页面,这里为 index.php。
令牌存储
你可以将令牌存储在 SessionStorage 或 PdoStorage 中。第一种已经在上文演示过,不需要进一步配置,它直接工作。
$tokenStorage = new SessionStorage();
PDO 后端需要你指定要使用的数据库。
$db = new PDO("sqlite:/path/to/db/client.sqlite");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$tokenStorage = new PdoStorage($db);
在两种情况下,你都可以在构造函数中使用 $tokenStorage,之前我们直接放置 new SessionStorage()。有关如何指定其他数据库的信息,请参阅 PHP PDO 文档。
请注意,如果你使用 SQLite,请注意你写入文件的 目录 也需要可以被 web 服务器写入!
日志记录
为了记录 OAuth 库对令牌端点的所有请求,你可以使用例如 Monolog 适配器来做到这一点。以下是如何做到这一点的示例。对于生产系统,你可能希望集成你自己的日志框架,设置适当的日志级别,例如仅记录错误。
所以,你不仅可以这样做
new Client()
你可以使用以下片段
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Guzzle\Plugin\Log\LogPlugin;
use Guzzle\Log\MessageFormatter;
use Guzzle\Log\MonologLogAdapter;
/* create the log channel */
$log = new Logger('my-app');
$log->pushHandler(new StreamHandler(sprintf("%s/data/client.log", __DIR__), Logger::DEBUG));
$logPlugin = new LogPlugin(new MonologLogAdapter($log), MessageFormatter::DEBUG_FORMAT);
$httpClient = new Client();
$httpClient->addSubscriber($logPlugin);
现在你可以将 $httpClient 传递给 Api 和 Callback 类,请求和响应包括其体将被记录。
API
可以使用 Sami 生成 API 文档,Sami 是 Composer 文件中的 require-dev 部分的一部分。
$ php vendor/bin/sami.php update doc/php-oauth-client.php
这将输出到 build/ 目录中的 HTML。
测试
要运行测试,你可以使用 PHPUnit。你可以这样运行测试
$ php /path/to/phpunit.phar tests
从目录中。确保在运行测试之前首先运行 php /path/to/composer.phar install。