ash-rain / oauth2-server
PHP OAuth 2.0 实现。
Requires
- symfony/http-foundation: ~2.4
Requires (Dev)
- mockery/mockery: ~0.9
- phpunit/phpunit: ~4.0
- predis/predis: ~0.8.5
Suggests
- predis/predis: Predis is required to use the Redis storage adapter.
This package is auto-updated.
Last update: 2024-09-07 19:38:52 UTC
README
PHP OAuth 2.0 服务器实现。
安装
该软件包可以通过 Composer 安装,直接修改 composer.json
文件或使用 composer require
命令。
composer require ash-rain/oauth2-server
请注意,此软件包仍在开发中,尚未标记为稳定版本。
授权类型
本软件包实现了规范中详细说明的四种 OAuth 2.0 授权类型。
存储适配器
从 v0.1.0 版本开始,以下存储适配器可用。
Dingo\OAuth2\Storage\MySqlAdapter
Dingo\OAuth2\Storage\RedisAdapter
使用 dingo/oauth2-server-laravel 软件包,您还可以使用。
Dingo\OAuth2\Storage\FluentAdapter
MySQL 表结构
以下是在 MySQL 存储适配器中所需的表结构。当开发自己的存储适配器时,您可以使用此结构作为起点。
CREATE TABLE IF NOT EXISTS `oauth_authorization_codes` ( `code` varchar(40) COLLATE utf8_unicode_ci NOT NULL, `client_id` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `user_id` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `redirect_uri` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, `expires` datetime NOT NULL, PRIMARY KEY (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; CREATE TABLE IF NOT EXISTS `oauth_authorization_code_scopes` ( `id` int(11) NOT NULL AUTO_INCREMENT, `code` varchar(40) COLLATE utf8_unicode_ci NOT NULL, `scope` varchar(255) COLLATE utf8_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `code` (`code`,`scope`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1; CREATE TABLE IF NOT EXISTS `oauth_clients` ( `id` varchar(40) COLLATE utf8_unicode_ci NOT NULL, `secret` varchar(40) COLLATE utf8_unicode_ci NOT NULL, `name` varchar(100) COLLATE utf8_unicode_ci NOT NULL, `trusted` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; CREATE TABLE IF NOT EXISTS `oauth_client_endpoints` ( `id` int(11) NOT NULL AUTO_INCREMENT, `client_id` varchar(40) COLLATE utf8_unicode_ci NOT NULL, `uri` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `is_default` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1; CREATE TABLE IF NOT EXISTS `oauth_scopes` ( `scope` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `description` text COLLATE utf8_unicode_ci NOT NULL, PRIMARY KEY (`scope`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; CREATE TABLE IF NOT EXISTS `oauth_tokens` ( `token` varchar(40) COLLATE utf8_unicode_ci NOT NULL, `type` enum('access','refresh') COLLATE utf8_unicode_ci NOT NULL DEFAULT 'access', `client_id` varchar(40) COLLATE utf8_unicode_ci NOT NULL, `user_id` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, `expires` datetime NOT NULL, PRIMARY KEY (`token`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; CREATE TABLE IF NOT EXISTS `oauth_token_scopes` ( `id` int(11) NOT NULL AUTO_INCREMENT, `token` varchar(40) COLLATE utf8_unicode_ci NOT NULL, `scope` varchar(255) COLLATE utf8_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `token` (`token`,`scope`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1;
使用说明
本指南非常简短,且与框架无关。其目的是简单地演示各个部分的组合,并不打算作为实际应用使用。因此,将不会提及代码的每个部分属于哪里。
在我们继续之前,您应该了解以下术语的含义。
客户端
在 OAuth 2.0 中,客户端是一个代表用户行事并与其他授权和资源服务器通信的应用程序。
该软件包可以创建客户端,但软件包不提供用户界面。要创建客户端,您需要一个存储适配器实例。
$storage = new Dingo\OAuth2\Storage\MySqlAdapter(new PDO('mysql:host=localhost;dbname=oauth', 'root'));
现在,您可以获取客户端存储并创建一个新的客户端。
$storage->get('client')->create('id', 'secret', 'name', [['uri' => 'http://example.com/code', 'default' => true]]);
客户端预期至少有一个关联的端点,并且应该将其定义为默认端点。客户端可以有多个端点用于测试或预发布服务器。
$storage->get('client')->create('id', 'secret', 'name', [ ['uri' => 'http://example.com/code', 'default' => true], ['uri' => 'http://staging.example.com/code', 'default' => false] ]);
客户端可以设置为“可信”,这意味着在授权之前可以快速检查,如果客户端标记为“可信”,则将自动授权。第五个参数必须设置为 true
以标记客户端为“可信”。
$storage->get('client')->create('id', 'secret', 'name', [['uri' => 'http://example.com/code', 'default' => true]], true);
您还可以删除客户端。这将删除相关的端点。
$storage->get('client')->delete('id');
在本指南的其余部分,将假设您已创建了一个类似于以下示例的客户端。
$storage->get('client')->create('id', 'secret', 'name', [['uri' => 'https:///example-client/auth/code', 'default' => true]]);
作用域
当客户端请求用户的授权时,客户端通常会请求特定的权限,这些权限称为 作用域。作用域定义了客户端可以查看或执行的内容。本质上,它们为开发者提供了更精细的控制,以限制客户端可以访问的内容。
该软件包可以创建作用域,但软件包不提供用户界面。要创建作用域,您需要一个存储适配器实例。
$storage = new Dingo\OAuth2\Storage\MySqlAdapter(new PDO('mysql:host=localhost;dbname=oauth', 'root'));
现在,您可以获取作用域存储并创建一个新的作用域。
$storage->get('scope')->create('scope', 'name', 'description');
您还可以删除作用域。
$storage->get('scope')->delete('scope');
本指南将不会使用作用域,但您可以自由创建和使用它们。
授权服务器
授权服务器的职责是授权并颁发访问令牌给客户端。根据配置,授权服务器还会颁发刷新令牌,客户端应将其存储起来以备访问令牌过期时使用。
要颁发访问令牌,授权服务器必须配置所需的存储适配器和授权类型。
$storage = new Dingo\OAuth2\Storage\MySqlAdapter(new PDO('mysql:host=localhost;dbname=oauth', 'root')); $server = new Dingo\OAuth2\Server\Authorization($storage);
现在我们可以根据项目需求注册四种不同的授权类型。在本指南中,我们只使用标准的授权代码授权类型。
$server->registerGrant(new Dingo\OAuth2\Grant\AuthorizationCode);
现在我们需要一个路由来处理尝试的授权。本指南假设这个路由在 https:///example-server/authorize
。
<?php // If the user is not logged in we'll redirect them to the login form // with the query string that was sent with the initial request. // The login form is not within the scope of this guide. if ( ! isset($_SESSION['user_id'])) { header("Location: /login?{$_SERVER['QUERY_STRING']}"); } else { try { $payload = $server->validateAuthorizationRequest(); } catch (Dingo\OAuth2\Exception\ClientException $exception) { echo $exception->getMessage(); exit; } if (isset($_POST['submit']) or $payload['client']->isTrusted()) { $response = $server->handleAuthorizationRequest($payload['client_id'], $_SESSION['user_id'], $payload['redirect_uri'], $payload['scopes']); header("Location: {$server->makeRedirectUri($response)}"); } else { ?> <p><?php echo $payload['client']->getName(); ?> wants permission to:</p> <table> <?php foreach($payload['scopes'] as $scope): ?> <tr> <td> <strong><?php echo $scope->getName(); ?></strong> </td> <td> <?php echo $scope->getDescription(); ?> </td> </tr> <?php endforeach; ?> </table> <form method="POST"> <input type="submit" name="submit" value="Authorize"> <input type="submit" name="cancel" value="Cancel"> </form> <?php } } ?>
客户端现在可以提示用户通过OAuth 2.0进行授权,通过将用户导向类似的URI(注意,为了可读性添加了空格)。
https:///example-server/authorize
?response_type=code
&client_id=example
&redirect_uri=http%3A%2F%2Flocalhost%2Fexample-client%2Fauth%2Fcode
如果授权服务器检测到用户未登录,他们将被重定向到登录页面并要求登录。一旦登录,用户应该被重定向回他们被提示授权客户端的地方,除非客户端已被标记为“可信”。如果用户授权客户端,授权服务器将发放一个授权代码,该代码作为查询字符串的一部分发送回提供的重定向URI。
请记住,如果提供了重定向URI,它必须匹配为客户端注册的重定向URI。如果没有提供重定向URI,则使用默认的重定向URI。
现在客户端有了授权代码,它需要使用该代码从授权服务器请求访问令牌。我们需要一个路由来处理访问令牌的发放。本指南假设该路由在 https:///example-server/token
。
header('Content-Type: application/json'); echo json_encode($server->issueAccessToken());
客户端现在可以通过向授权服务器发送另一个请求到类似的URI来请求访问令牌(注意,为了可读性添加了空格)。
https:///example-server/token
?grant_type=authorization_code
&code=<authorization_code_returned_by_server>
&client_id=example
&client_secret=topsecret
授权服务器应该响应一个类似于以下的JSON负载。
{ "access_token": "nkwCbxJ8EAEqEM11vCrKLd2TAqJLfCN21beMjVGK", "token_type": "Bearer", "expires": 1396795320, "expires_in": 3600, "refresh_token": "vnzKgulkldV1cnDeVh4y8KbAjDHCqvWBMnxTUqWa" }
客户端应该保存刷新令牌,并且所有后续请求受保护资源时都应该包含一个类似于以下的 Authorization
标头。
Authorization: Bearer nkwCbxJ8EAEqEM11vCrKLd2TAqJLfCN21beMjVGK
授权回调
您可以设置一个可选的授权回调,一旦客户端已被授权,该回调就会触发。当您想要避免提示用户授权以前已经使用相同作用域授权过的客户端时,这非常有用。回调接收两个参数,第一个是 Dingo\OAuth2\Entity\Token
的实例,第二个是 Dingo\OAuth2\Entity\Client
的实例。
$server->setAuthorizedCallback(function($token, $client) { // Insert a record into your database showing that $token->getUserId() has authorized // $client->getId() with $token->getScopes() and that in the future the server // can skip the prompt. });
检查用户是否以前已经授权过客户端的代码取决于您。例如,您可能调整之前的 if
语句以检查数据库记录的存在。
$alreadyAuthorized = $db->table('user_authorized_clients') ->where('client_id', '=', $payload['client']->getId()) ->where('user_id', '=', $_SESSION['user']['id']) ->exists(); if (isset($_POST['submit']) or $alreadyAuthorized == true) { $response = $server->handleAuthorizationRequest($payload['client_id'], $payload['user_id'], $payload['redirect_uri'], $payload['scopes']); header("Location: {$server->makeRedirectUri($response)}"); }
请确保只授权具有相同作用域或更低的作用域的请求。永远不要授权包含不同作用域的请求,因为用户应该有机会审查并批准或拒绝请求。
资源服务器
资源服务器的责任是通过对提供的访问令牌进行验证来验证请求。
$storage = new Dingo\OAuth2\Storage\MySqlAdapter(new PDO('mysql:host=localhost;dbname=oauth', 'root')); $server = new Dingo\OAuth2\Server\Resource($storage);
现在我们可以验证请求是否包含一个存在且未过期的访问令牌。
try { $server->validateRequest(); } catch (Dingo\OAuth2\Exception\InvalidTokenException $exception) { header('Content-Type: application/json', true, $exception->getStatusCode()); echo json_encode(['error' => $exception->getError(), 'message' => $exception->getMessage()]); exit; }
异常错误
在发放访问令牌或验证受保护资源的请求时,如果出现问题(例如,请求中缺少必需的参数),可能会抛出异常。一般来说,所有抛出的异常都将扩展自 Dingo\OAuth2\Exception\OAuthException
。每个异常都将有一个错误类型、一条消息和一个HTTP状态码。
这允许您向客户端返回更详细的信息。同时返回错误类型和错误消息便于客户端区分实际情况并相应地做出反应。
请参考上述 资源服务器 示例,了解如何捕获异常并将其返回给客户端。
错误类型
以下表格描述了错误类型以及为什么提出这个错误的原因。