OAuth 2.0
不太熟悉什么是OAuth2.0的同学可以参考阮大神的文章, 理解OAuth 2.0 - 阮一峰
授权码模式(Authorization Code)
# 授权代码授予类型用于获得访问权限令牌和刷新令牌,并为机密客户进行了优化。
# 由于这是一个基于重定向的流程,客户端必须能够与资源所有者的用户代理(通常是Web)交互浏览器),能够接收传入请求(通过重定向)从授权服务器。
# 授权代码流如下:
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
授权码授权开发
引入OAuth-server包
# PHP 5.3.9+
composer require bshaffer/oauth2-server-php "^1.10"
创建数据表
-- 你可使用相应的数据库引擎:MySQL / SQLite / PostgreSQL / MS SQL Server
-- 数据库:oauth_test
-- 细调过表相关结构,不过你也可以参考官方:http://bshaffer.github.io/oauth2-server-php-docs/cookbook/
DROP TABLE IF EXISTS `oauth_access_tokens`;
CREATE TABLE `oauth_access_tokens` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`access_token` varchar(40) NOT NULL,
`client_id` varchar(80) NOT NULL,
`user_id` varchar(80) DEFAULT NULL,
`expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`scope` text NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `IDX_ACCESS_TOKEN` (`access_token`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for oauth_authorization_codes
-- ----------------------------
DROP TABLE IF EXISTS `oauth_authorization_codes`;
CREATE TABLE `oauth_authorization_codes` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`authorization_code` varchar(40) DEFAULT '',
`client_id` varchar(80) DEFAULT '',
`user_id` varchar(80) DEFAULT '0',
`redirect_uri` varchar(2000) DEFAULT '',
`expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`scope` text,
`id_token` varchar(1000) DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `IDX_CODE` (`authorization_code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of oauth_authorization_codes
-- ----------------------------
-- ----------------------------
-- Table structure for oauth_clients
-- ----------------------------
DROP TABLE IF EXISTS `oauth_clients`;
CREATE TABLE `oauth_clients` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`client_id` varchar(80) DEFAULT '',
`client_secret` varchar(80) DEFAULT '',
`client_name` varchar(120) DEFAULT '',
`redirect_uri` varchar(2000) DEFAULT '',
`grant_types` varchar(80) DEFAULT '',
`scope` varchar(4000) DEFAULT '',
`user_id` varchar(80) DEFAULT '0',
PRIMARY KEY (`id`),
KEY `IDX_APP_SECRET` (`client_id`,`client_secret`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of oauth_clients
-- ----------------------------
INSERT INTO `oauth_clients` VALUES ('1', 'testclient', '123456', '测试demo', 'http://sxx.qkl.local/v2/oauth/cb', 'authorization_code refresh_token', 'basic get_user_info upload_pic', '');
-- ----------------------------
-- Table structure for oauth_jwt
-- ----------------------------
DROP TABLE IF EXISTS `oauth_jwt`;
CREATE TABLE `oauth_jwt` (
`client_id` varchar(80) NOT NULL,
`subject` varchar(80) DEFAULT NULL,
`public_key` varchar(2000) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of oauth_jwt
-- ----------------------------
-- ----------------------------
-- Table structure for oauth_public_keys
-- ----------------------------
DROP TABLE IF EXISTS `oauth_public_keys`;
CREATE TABLE `oauth_public_keys` (
`client_id` varchar(80) DEFAULT NULL,
`public_key` varchar(2000) DEFAULT NULL,
`private_key` varchar(2000) DEFAULT NULL,
`encryption_algorithm` varchar(100) DEFAULT 'RS256'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of oauth_public_keys
-- ----------------------------
-- ----------------------------
-- Table structure for oauth_refresh_tokens
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_tokens`;
CREATE TABLE `oauth_refresh_tokens` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`refresh_token` varchar(40) NOT NULL,
`client_id` varchar(80) NOT NULL DEFAULT '',
`user_id` varchar(80) DEFAULT '',
`expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`scope` text,
PRIMARY KEY (`id`),
UNIQUE KEY `IDX_REFRESH_TOKEN` (`refresh_token`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for oauth_scopes
-- ----------------------------
DROP TABLE IF EXISTS `oauth_scopes`;
CREATE TABLE `oauth_scopes` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`scope` varchar(80) NOT NULL DEFAULT '',
`is_default` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of oauth_scopes
-- ----------------------------
INSERT INTO `oauth_scopes` VALUES ('1', 'basic', '1');
INSERT INTO `oauth_scopes` VALUES ('2', 'get_user_info', '0');
INSERT INTO `oauth_scopes` VALUES ('3', 'upload_pic', '0');
-- ----------------------------
-- Table structure for oauth_users 该表是Resource Owner Password Credentials Grant所使用
-- ----------------------------
DROP TABLE IF EXISTS `oauth_users`;
CREATE TABLE `oauth_users` (
`uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(80) DEFAULT '',
`password` varchar(80) DEFAULT '',
`first_name` varchar(80) DEFAULT '',
`last_name` varchar(80) DEFAULT '',
`email` varchar(80) DEFAULT '',
`email_verified` tinyint(1) DEFAULT '0',
`scope` text,
PRIMARY KEY (`uid`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of oauth_users
-- ----------------------------
INSERT INTO `oauth_users` VALUES ('1', 'qkl', '123456', 'kl', 'q', '', '', '');
创建server
Authorization Server 角色
public function _initialize()
{
require_once dirname(APP_PATH) . "/vendor/autoload.php";
Autoloader::register();
}
private function server()
{
$pdo = new \PDO('mysql:host=ip;dbname=oauth_test', "user", "123456");
//创建存储的方式
$storage = new \OAuth2\Storage\Pdo($pdo);
//创建server
$server = new \OAuth2\Server($storage);
// 添加 Authorization Code 授予类型
$server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($storage));
return $server;
}
创建授权页面(基于浏览器)
Authorization Server 角色
User Agent 角色,常规一般基于浏览器
// 授权页面和授权
public function authorize()
{
// 该页面请求地址类似:
// http://sxx.qkl.local/v2/oauth/authorize?response_type=code&client_id=testclient&state=xyz&redirect_uri=http://sxx.qkl.local/v2/oauth/cb&scope=basic%20get_user_info%20upload_pic
//获取server对象
$server = $this->server();
$request = \OAuth2\Request::createFromGlobals();
$response = new \OAuth2\Response();
// 验证 authorize request
// 这里会验证client_id,redirect_uri等参数和client是否有scope
if (!$server->validateAuthorizeRequest($request, $response)) {
$response->send();
die;
}
// 显示授权登录页面
if (empty($_POST)) {
//获取client类型的storage
//不过这里我们在server里设置了storage,其实都是一样的storage->pdo.mysql
$pdo = $server->getStorage('client');
//获取oauth_clients表的对应的client应用的数据
$clientInfo = $pdo->getClientDetails($request->query('client_id'));
$this->assign('clientInfo', $clientInfo);
$this->display('authorize');
die();
}
$is_authorized = true;
// 当然这部分常规是基于自己现有的帐号系统验证
if (!$uid = $this->checkLogin($request)) {
$is_authorized = false;
}
// 这里是授权获取code,并拼接Location地址返回相应
// Location的地址类似:http://sxx.qkl.local/v2/oauth/cb?code=69d78ea06b5ee41acbb9dfb90500823c8ac0241d&state=xyz
// 这里的$uid不是上面oauth_users表的uid, 是自己系统里的帐号的id,你也可以省略该参数
$server->handleAuthorizeRequest($request, $response, $is_authorized, $uid);
// if ($is_authorized) {
// // 这里会创建Location跳转,你可以直接获取相关的跳转url,用于debug
// $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=')+5, 40);
// exit("SUCCESS! Authorization Code: $code :: " . $response->getHttpHeader('Location'));
// }
$response->send();
}
/**
* 具体基于自己现有的帐号系统验证
* @param $request
* @return bool
*/
private function checkLogin($request)
{
//todo
if ($request->request('username') != 'qkl') {
return $uid = 0; //login faile
}
return $uid = 1; //login success
}
创建获取token
Authorization Server 角色
// 生成并获取token
public function token()
{
$server = $this->server();
$server->handleTokenRequest(\OAuth2\Request::createFromGlobals())->send();
exit();
}
授权页面
CLIENT 客户端 角色
# 浏览器访问:
http://sxx.qkl.local/v2/oauth/authorize?response_type=code&client_id=testclient&state=xyz&redirect_uri=http://sxx.qkl.local/v2/oauth/cb&scope=basic%20get_user_info%20upload_pic
授权页面说明
# 我们换行分解下
http://sxx.qkl.local/v2/oauth/authorize?
# response_type 固定写死 code
response_type=code&
# client_id 我们oauth_clients表的client_id值
client_id=testclient&
# state 自定义的参数,随意字符串值
state=xyz&
# redirect_uri 回调地址,这里最好是urlencode编码,我这里演示没编码
# 注意这里的redirect_uri需要和oauth_clients表的redirect_uri字段做匹配处理
# redirect_uri字段可存取的方式:
# 1. http://sxx.qkl.local/v2/oauth/cb
# 2. http://sxx.qkl.local/v2/oauth/cb http://sxx.qkl.local/v2/oauth/cb2 ... 空格分割
redirect_uri=http://sxx.qkl.local/v2/oauth/cb&
# response_type 固定写死 code
scope=basic%20get_user_info%20upload_pic
客户端获取code并请求获取access_token
CLIENT 客户端 角色
// 客户端回调,来自server端的Location跳转到此
// 此处会携带上code和你自定义的state
public function cb()
{
$request = \OAuth2\Request::createFromGlobals();
$url = "http://sxx.qkl.local/v2/oauth/token";
$data = [
'grant_type' => 'authorization_code',
'code' => $request->query('code'),
'client_id' => 'testclient',
'client_secret' => '123456',
'redirect_uri' => 'http://sxx.qkl.local/v2/oauth/cb'
];
//todo 自定义的处理判断
$state = $request->query('state');
$response = Curl::ihttp_post($url, $data);
if (is_error($response)) {
var_dump($response);
}
var_dump($response['content']);
}
刷新token
Authorization Server 角色
// 创建刷新token的server
private function refresh_token_server()
{
$pdo = new \PDO('mysql:host=ip;dbname=oauth_test', "user", "123456");
$storage = new \OAuth2\Storage\Pdo($pdo);
$config = [
'always_issue_new_refresh_token' => true,
'refresh_token_lifetime' => 2419200,
];
$server = new \OAuth2\Server($storage, $config);
// 添加一个 RefreshToken 的类型
$server->addGrantType(new \OAuth2\GrantType\RefreshToken($storage, [
'always_issue_new_refresh_token' => true
]));
// 添加一个token的Response
$server->addResponseType(new \OAuth2\ResponseType\AccessToken($storage, $storage, [
'refresh_token_lifetime' => 2419200,
]));
return $server;
}
// 刷新token
public function refresh_token()
{
$server = $this->refresh_token_server();
$server->handleTokenRequest(\OAuth2\Request::createFromGlobals())->send();
exit();
}
客户端请求refresh_token
CLIENT 客户端 角色
// 客户端模拟refresh_token
public function client_refresh_token()
{
$request = \OAuth2\Request::createFromGlobals();
$url = "http://sxx.qkl.local/v2/oauth/refresh_token";
$data = [
'grant_type' => 'refresh_token',
'refresh_token' => 'd9c5bee6a4ad7967ac044c99e40496aa2c3d28b4',
'client_id' => 'testclient',
'client_secret' => '123456'
];
$response = Curl::ihttp_post($url, $data);
if (is_error($response)) {
var_dump($response);
}
var_dump($response['content']);
}
scope授权资源
Authorization Server 角色
这里说明下 因为在上面表创建时,我创建了3个socpe[basic,get_user_info,upload_pic]用于测试
上面我们在浏览器访问的授权地址上也填写了三个权限,所以只要access_token正确在时效内,即可成功访问
// 测试资源
public function res1()
{
$server = $this->server();
// Handle a request to a resource and authenticate the access token
if (!$server->verifyResourceRequest(\OAuth2\Request::createFromGlobals())) {
$server->getResponse()->send();
die;
}
$token = $server->getAccessTokenData(\OAuth2\Request::createFromGlobals());
$scopes = explode(" ", $token['scope']);
// todo 这里你可以写成自己规则的scope验证
if (!$this->checkScope('basic', $scopes)) {
$this->ajaxReturn(['success' => false, 'message' => '你没有获取该接口的scope']);
}
$this->ajaxReturn(['success' => true, 'message' => '你成功获取该接口信息', 'token'=>$token['user_id']]);
}
// 用于演示检测scope的方法
private function checkScope($myScope, $scopes)
{
return in_array($myScope, $scopes);
}
客户端postman模拟测试
总结
Oauth2.0整体没什么具体的技术含量,可以参照规范实现即可
后续
PHP下的Oauth2.0尝试 - OpenID Connect - 后续补位
附录
Oauth2.0 - Authorization Code Grant
使用Authorization_Code获取Access_Token - QQ互联接入
推荐阅读登录授权方案 - 网站的无密码登录 - 阮一峰