dingo/oauth2-server

PHP OAuth 2.0 实现库。

v0.2.2 2014-04-24 06:34 UTC

This package is auto-updated.

Last update: 2024-08-24 13:30:53 UTC


README

A PHP OAuth 2.0 服务器实现。

Build Status

安装

该包可以使用Composer安装,直接修改composer.json或使用composer require命令。

composer require dingo/oauth2-server:0.1.*

注意,此包仍在开发中,尚未标记为稳定。

授权类型

该包实现了规范中详细说明的四种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' => 'http://localhost/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);

我们现在需要一个路由来处理尝试授权。本指南假设这个路由在 http://localhost/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(注意为了可读性添加了空格)。

http://localhost/example-server/authorize
	?response_type=code
	&client_id=example
	&redirect_uri=http%3A%2F%2Flocalhost%2Fexample-client%2Fauth%2Fcode

如果授权服务器检测到用户未登录,则会将用户重定向到登录页面并要求登录。登录后,用户应该被重定向回之前的位置,在那里提示用户授权客户端(除非客户端已被标记为“可信”)。如果用户授权了客户端,授权服务器将颁发一个授权代码,该代码作为提供的重定向URI查询字符串的一部分返回。

记住,如果提供了重定向URI,它必须与为客户端注册的重定向URI相匹配。如果没有提供重定向URI,则使用默认的重定向URI。

现在客户端有了授权代码,它需要使用该代码从授权服务器请求访问令牌。我们需要一个处理颁发访问令牌的路由。本指南假设该路由在 http://localhost/example-server/token

header('Content-Type: application/json');

echo json_encode($server->issueAccessToken());

客户端现在可以通过向授权服务器发送另一个请求到如下类似的URI来请求访问令牌(注意为了可读性添加了空格)。

http://localhost/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状态码。

这允许您向客户端返回更详细的信息。同时返回错误类型和错误消息方便客户端区分实际发生的情况并做出相应的反应。

参考上述 资源服务器 示例以了解如何捕获和向客户端返回异常。

错误类型

以下表格描述了错误类型以及为什么会产生此错误。