OAuth 2.0 Server 和 Client

OAuth 2.0 是一种授权框架,允许第三方应用以有限的权限代表用户访问资源。OAuth 2.0 是对 OAuth 1.0 的改进,简化了授权过程并增强了安全性。为了更好地理解 OAuth 2.0 的工作机制,我们需要了解 OAuth 2.0 Server 和 OAuth 2.0 Client 的角色和功能。

OAuth 2.0 Server

OAuth 2.0 Server,又称为授权服务器,是整个授权流程的核心部分。它负责认证用户并颁发令牌,以便客户端应用可以在用户的许可下访问资源。

功能

用户认证:OAuth 2.0 Server 负责验证用户的身份。这通常通过用户名和密码,或者多因素认证(MFA)来完成。
授权:用户通过 OAuth 2.0 Server 授权客户端应用访问其资源。授权过程通常通过一个用户同意页面完成,用户可以在该页面上选择允许或拒绝客户端请求的权限。
令牌颁发:在用户同意授权后,OAuth 2.0 Server 会生成一个访问令牌(Access Token)和可选的刷新令牌(Refresh Token)。访问令牌用于访问受保护的资源,而刷新令牌用于获取新的访问令牌。
令牌管理:OAuth 2.0 Server 负责管理和验证令牌的有效性。它会存储令牌并在需要时验证令牌,以确保它们未被篡改或过期。
错误处理:OAuth 2.0 Server 还负责处理各种错误情况,例如用户认证失败、权限不足或令牌无效等,并返回相应的错误消息给客户端。

示例

假设我们有一个照片共享应用程序 PhotoShare,它允许用户将他们的照片上传到云端,并与朋友分享。PhotoShare 决定使用 OAuth 2.0 来授权第三方应用访问用户的照片。以下是一个 OAuth 2.0 Server 的工作示例:

用户请求:用户打开一个第三方应用 PhotoEditor,该应用希望访问 PhotoShare 上的用户照片。
重定向到授权服务器:PhotoEditor 将用户重定向到 PhotoShare 的 OAuth 2.0 Server。
用户登录和授权:用户在 PhotoShare 的 OAuth 2.0 Server 上登录,并被提示是否允许 PhotoEditor 访问他们的照片。用户点击“允许”。
授权码:PhotoShare 的 OAuth 2.0 Server 生成一个授权码,并将用户重定向回 PhotoEditor,同时附带该授权码。
获取令牌:PhotoEditor 使用授权码向 PhotoShare 的 OAuth 2.0 Server 请求访问令牌。
访问令牌:PhotoShare 的 OAuth 2.0 Server 验证授权码并返回访问令牌。
访问资源:PhotoEditor 使用访问令牌访问用户在 PhotoShare 上的照片。

OAuth 2.0 Client

OAuth 2.0 Client 是需要访问受保护资源的应用程序。在 OAuth 2.0 中,客户端应用通过 OAuth 2.0 Server 获取访问令牌,以便代表用户访问资源。

功能

重定向用户:OAuth 2.0 Client 负责将用户重定向到 OAuth 2.0 Server 的授权端点,以便用户进行认证和授权。
处理授权码:当用户在 OAuth 2.0 Server 上成功授权后,OAuth 2.0 Client 会接收一个授权码,并使用该授权码向 OAuth 2.0 Server 请求访问令牌。
存储令牌:OAuth 2.0 Client 需要安全地存储访问令牌和刷新令牌,以便在需要时使用。
使用令牌访问资源:OAuth 2.0 Client 使用获取的访问令牌向资源服务器发送请求,以访问受保护的资源。
刷新令牌:如果访问令牌过期,OAuth 2.0 Client 可以使用刷新令牌获取新的访问令牌,而无需用户重新进行认证和授权。

示例

继续上述的 PhotoShare 示例,假设 PhotoEditor 是一个希望编辑用户照片的应用程序。以下是一个 OAuth 2.0 Client 的工作示例:

重定向用户:PhotoEditor 需要访问 PhotoShare 上的用户照片,所以它将用户重定向到 PhotoShare 的 OAuth 2.0 Server 进行认证和授权。
处理授权码:用户在 PhotoShare 的 OAuth 2.0 Server 上授权后,PhotoEditor 接收到一个授权码。
请求令牌:PhotoEditor 使用授权码向 PhotoShare 的 OAuth 2.0 Server 请求访问令牌。
存储令牌:PhotoEditor 接收到访问令牌并安全地存储它。
访问资源:PhotoEditor 使用访问令牌向 PhotoShare 的资源服务器发送请求,以访问用户的照片。
刷新令牌:如果访问令牌过期,PhotoEditor 使用刷新令牌获取新的访问令牌,以继续访问用户的照片。

OAuth 2.0 流程类型

OAuth 2.0 定义了几种不同的授权流程,以适应不同类型的客户端应用和使用场景。主要的授权流程包括:

授权码流程(Authorization Code Grant):这种流程适用于服务器端应用程序,通常通过重定向和授权码交换来获取访问令牌。
简化流程(Implicit Grant):这种流程适用于单页应用程序(SPA)和移动应用程序,直接获取访问令牌,而不使用授权码。
密码凭证流程(Resource Owner Password Credentials Grant):这种流程适用于受信任的客户端应用程序,用户直接向客户端提供用户名和密码,客户端使用这些凭证向 OAuth 2.0 Server 请求访问令牌。
客户端凭证流程(Client Credentials Grant):这种流程适用于应用程序之间的访问,客户端使用自己的凭证(而不是用户的凭证)向 OAuth 2.0 Server 请求访问令牌。

安全性考量

OAuth 2.0 提供了一些安全性措施,以确保授权过程的安全性和数据的保护:

HTTPS:所有 OAuth 2.0 请求和响应都应该使用 HTTPS 传输,以保护敏感信息免受窃听和中间人攻击。
状态参数:在授权请求中使用状态参数,可以防止跨站请求伪造(CSRF)攻击。OAuth 2.0 Client 可以在授权请求中包含一个随机生成的状态参数,并在重定向返回时验证该参数。
令牌有效期:访问令牌和刷新令牌都应该设置合理的有效期,以减少令牌被滥用的风险。
范围限制:OAuth 2.0 允许客户端应用请求特定的权限范围(Scope),以限制客户端应用可以访问的资源和操作。

总结

OAuth 2.0 Server 和 OAuth 2.0 Client 在授权流程中扮演着关键角色。OAuth 2.0 Server 负责用户认证、授权、令牌颁发和管理,而 OAuth 2.0 Client 则负责引导用户完成授权流程并使用获取的令牌访问受保护的资源。通过了解 OAuth 2.0 的工作机制和各种授权流程,我们可以更好地设计和实现安全、高效的授权系统。

OAuth 2.0 的应用场景非常广泛,包括社交媒体平台、云存储服务、金融应用等。通过使用 OAuth 2.0,应用程序可以在确保安全性的同时,实现更灵活和便捷的用户授权体验。


实现示例

第一步
A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。

`https://b.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
`

response_type参数表示要求返回授权码(code),client_id参数让 B 知道是谁在请求,redirect_uri参数是 B 接受或拒绝请求后的跳转网址,scope参数表示要求的授权范围(这里是只读)。执行成功后,B.com往数据库的oauth_authorization_codes写入对应数据。

第二步
用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码,就像下面这样。

https://a.com/callback?code=AUTHORIZATION_CODE

第三步
A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。

`https://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=CALLBACK_URL
`

client_id参数和client_secret参数用来让 B 确认 A 的身份(client_secret参数是保密的,因此只能在后端发请求),grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码,code参数是上一步拿到的授权码,redirect_uri参数是令牌颁发后的回调网址。

第四步
B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。

{    
  "access_token":"ACCESS_TOKEN",
  "token_type":"bearer",
  "expires_in":2592000,
  "refresh_token":"REFRESH_TOKEN",
  "scope":"read",
  "uid":100101,
  "info":{...}
}

OAuth 2.0 Server 和 Client

可选方案

  • thephpleague / oauth2-server
  • thephpleague / oauth2-client
  • bshaffer / oauth2-server-php

代码示例

bshaffer / oauth2-server-php 为例
  1. 下载oauth-server-php: git clone https://github.com/bshaffer/oauth2-server-php.git -b master 存放到根目录
  2. 安装数据库

--
-- Database: `oauth2db`
--

-- --------------------------------------------------------

--
-- 表的结构 `oauth_access_tokens`
--

CREATE TABLE IF NOT EXISTS `oauth_access_tokens` (
  `access_token` varchar(40) NOT NULL,
  `client_id` varchar(80) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL,
  `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `scope` varchar(2000) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `oauth_authorization_codes`
--

CREATE TABLE IF NOT EXISTS `oauth_authorization_codes` (
  `authorization_code` varchar(40) NOT NULL,
  `client_id` varchar(80) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL,
  `redirect_uri` varchar(2000) DEFAULT NULL,
  `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `scope` varchar(2000) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `oauth_clients`
--

CREATE TABLE IF NOT EXISTS `oauth_clients` (
  `client_id` varchar(80) NOT NULL,
  `client_secret` varchar(80) NOT NULL,
  `redirect_uri` varchar(2000) NOT NULL,
  `grant_types` varchar(80) DEFAULT NULL,
  `scope` varchar(100) DEFAULT NULL,
  `user_id` varchar(80) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- 转存表中的数据 `oauth_clients`
--

INSERT INTO `oauth_clients` (`client_id`, `client_secret`, `redirect_uri`, `grant_types`, `scope`, `user_id`) VALUES
('testclient', 'testpass', 'https://user.endv.cn/', 'authorization_code', '', '');

-- --------------------------------------------------------

--
-- 表的结构 `oauth_jwt`
--

CREATE TABLE IF NOT EXISTS `oauth_jwt` (
  `client_id` varchar(80) NOT NULL,
  `subject` varchar(80) DEFAULT NULL,
  `public_key` varchar(2000) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `oauth_refresh_tokens`
--

CREATE TABLE IF NOT EXISTS `oauth_refresh_tokens` (
  `refresh_token` varchar(40) NOT NULL,
  `client_id` varchar(80) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL,
  `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `scope` varchar(2000) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `oauth_scopes`
--

CREATE TABLE IF NOT EXISTS `oauth_scopes` (
  `scope` text,
  `is_default` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `oauth_users`
--

CREATE TABLE IF NOT EXISTS `oauth_users` (
  `username` varchar(255) NOT NULL,
  `password` varchar(2000) DEFAULT NULL,
  `first_name` varchar(255) DEFAULT NULL,
  `last_name` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  1. 写入数据到数据库

添加客户端到表:oauth_clients,只有添加到该表到客户端才可以获取授权码

+------------+---------------+-------------------+--------------------+-------+---------+
| client_id  | client_secret | redirect_uri      | grant_types        | scope | user_id |
+------------+---------------+-------------------+--------------------+-------+---------+
| testclient | testpass      | http://127.0.0.1/ | authorization_code |       |         |
+------------+---------------+-------------------+--------------------+-------+---------+
  1. 配置server服务
    在根目录新建server.php文件,代码如下:
<?php
  /** 配置 */
$dsn= 'mysql:dbname=test;host=localhost';
$username = 'test';
$password = 'test';

// 错误报告(这毕竟是一个演示!)
ini_set('display_errors',1);error_reporting(E_ALL);

// 自动加载
require_once('oauth2-server-php/src/OAuth2/Autoloader.php');
OAuth2\Autoloader::register();
$storage = new OAuth2\Storage\Pdo(array('dsn' => $dsn, 'username' => $username, 'password' => $password));

// 通过存储对象或对象数组存储的oauth2服务器类
$server = new OAuth2\Server($storage);

// 授权码 有效期只有30秒
$server->addGrantType(new OAuth2\GrantType\AuthorizationCode($storage));

// 客户端证书
$server->addGrantType(new OAuth2\GrantType\ClientCredentials($storage));

// 用户凭据
$server->addGrantType(new OAuth2\GrantType\UserCredentials($storage));
// 刷新令牌  启用这个会报错,原因未知
// $server->addGrantType(new OAuth2\GrantType\RefreshToken($refreshStorage))
  1. 获取授权码
    参考oauth的授权码模式的使用的第一步。A网站向B网站请求授权码,B网站生成授权码并写入表oauth_authorization_codes中,然后返回给A网站
  2. 使用授权码获取令牌
    创建token.php文件,代码如下:
<?php
 // include our OAuth2 Server object
require_once __DIR__.'/server.php';
 
$server->handleTokenRequest(OAuth2\Request::createFromGlobals())->send();

然后执行指令:
curl -u testclient:testpass http://127.0.0.1/token.php --data "grant_type=authorization_code&code=你的授权码"
执行成功后则会返回数据:

{"access_token":"84c66d296308aad20aa5e065743d2fe30426b046","expires_in":3600,"token_type":"Bearer","scope":null,"refresh_token":"d49cd4d7d875065888fc457c7c714cab9fcf9d69"}
  1. 使用token获取数据
    创建resource.php文件,代码如下:

    <?php
     
     //资源控制器的建立和测试 
    require_once __DIR__.'/server.php';
    
    if (!$server->verifyResourceRequest(OAuth2\Request::createFromGlobals())) {
     $server->getResponse()->send();
     die;
    }
    $token = $server->getAccessTokenData(OAuth2\Request::createFromGlobals());
    echo "User ID associated with this token is {$token['user_id']}";
    
    echo json_encode(array('success' => true, 'message' => '您访问了我的API!'));
    

执行指令:
curl https://127.0.0.1/resource.php --data 'access_token=YOUR_TOKEN'

在这个resource.php文件中可以根据token的正确性来决定是否返回相应的数据给请求方。

参考:

Tags: oAuth

添加新评论