oauth2-server-php for windows 的那些坑
在windwos 环境下,使用vs2017 for php 工具进行调试时,总是搞不出来,
于是分析了一下原因,
首先,oauth2-server-php的环境是linux 而我在windows 环境下,步骤上是有些区别,但也不至于没办法调试
下面就分析一下每个步骤的细微区别“
1、windows 源码地址是,https://github.com/bshaffer/oauth2-server-php/releases 下载最新版本的,基本类同,
https://github.com/bshaffer/oauth2-server-php/releases
OAuth和OpenID的区别:
OAuth关注的是authorization授权,即:'用户能做什么';而OpenID侧重的是authentication认证,即:'用户是谁'。OpenID是用来认证协议,OAuth是授权协议,二者是互补的
OAuth 2.0将分为两个角色: Authorization server负责获取用户的授权并且发布token; Resource负责处理API calls。
如果用户的照片在A网站,他想要在B网站使用A网站的头像,并不需要向B网站提供自己在A网站的用户名和密码,而直接给B一个Access Token来获取A站的照片
具体流程如下:
1)用户访问网站B
2)B需要验证用户的身份
3)B将用户定向到A网站,用户输入帐号密码登录A网站
4)A网站询问是否要将Authentication的权利给B网站
5)用户告诉A站可以将认证权给B站
6)A网站把Authorization Code发给B站
7)B站用Autorization Code向A站换取Access Token
8)当B站拥有Access Token时,就拥有了用户在A站的一些访问权限
这是典型的Authorization Code Grant,常常运用于网络应用之中
还有Implicit Grant认证方式,这个则省去了AuthCode,开放平台直接返回access_token和有效期,用户ID等数据这种经常运用于手机客户端或者浏览器插件等没有在线服务器的应用
最后一种是Resource Owner Password Credentials Grant
这种是直接在应用中输入帐号密码,然后由应用XAuth技术将其提交给开放平台并得到Access Token
它经常用于PC可执行程序和手机应用,但由于存在一些争议,开发难度也较大,这里我就先不讨论他
使用 OAuth2-Server-php
1 CREATE TABLE `oauth_access_tokens` ( 2 `access_token` varchar(40) NOT NULL, 3 `client_id` varchar(80) NOT NULL, 4 `user_id` varchar(255) DEFAULT NULL, 5 `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 6 `scope` varchar(2000) DEFAULT NULL, 7 PRIMARY KEY (`access_token`) 8 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 9 10 CREATE TABLE `oauth_authorization_codes` ( 11 `authorization_code` varchar(40) NOT NULL, 12 `client_id` varchar(80) NOT NULL, 13 `user_id` varchar(255) DEFAULT NULL, 14 `redirect_uri` varchar(2000) DEFAULT NULL, 15 `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 16 `scope` varchar(2000) DEFAULT NULL, 17 PRIMARY KEY (`authorization_code`) 18 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 19 20 CREATE TABLE `oauth_clients` ( 21 `client_id` varchar(80) NOT NULL, 22 `client_secret` varchar(80) NOT NULL, 23 `redirect_uri` varchar(2000) NOT NULL, 24 `grant_types` varchar(80) DEFAULT NULL, 25 `scope` varchar(100) DEFAULT NULL, 26 `user_id` varchar(80) DEFAULT NULL, 27 PRIMARY KEY (`client_id`) 28 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 29 30 CREATE TABLE `oauth_refresh_tokens` ( 31 `refresh_token` varchar(40) NOT NULL, 32 `client_id` varchar(80) NOT NULL, 33 `user_id` varchar(255) DEFAULT NULL, 34 `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 35 `scope` varchar(2000) DEFAULT NULL, 36 PRIMARY KEY (`refresh_token`) 37 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 38 39 CREATE TABLE `oauth_users` ( 40 `user_id` int(11) NOT NULL AUTO_INCREMENT, 41 `username` varchar(255) NOT NULL, 42 `password` varchar(2000) DEFAULT NULL, 43 `first_name` varchar(255) DEFAULT NULL, 44 `last_name` varchar(255) DEFAULT NULL, 45 PRIMARY KEY (`user_id`) 46 ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 47 48 CREATE TABLE `oauth_scopes` ( 49 `scope` text, 50 `is_default` tinyint(1) DEFAULT NULL 51 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 52 53 CREATE TABLE `oauth_jwt` ( 54 `client_id` varchar(80) NOT NULL, 55 `subject` varchar(80) DEFAULT NULL, 56 `public_key` varchar(2000) DEFAULT NULL, 57 PRIMARY KEY (`client_id`) 58 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 59 -- test data 60 INSERT INTO oauth_clients (client_id, client_secret, redirect_uri) VALUES ("testclient", "testpass", "http://www.baidu.com/"); 61 INSERT INTO oauth_users (username, password, first_name, last_name) VALUES ('rereadyou', '8551be07bab21f3933e8177538d411e43b78dbcc', 'bo', 'zhang');
各表的名字说明了表中存取的内容,表名可自定义,自定义位置为:OAuth2/Storage/Pdo.php 74行的 config 数组中,因为这里采用的是 mysql 数据库,所以需要修改的是 Pdo,若是采用其它的存储方案,如 Redis,则自行修改对应文件即可。注意这里的数据库名称是都是单数形式
/Pdo.php
1 // debugging 2 $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 3 4 $this->config = array_merge(array( 5 'client_table' => 'oauth_clients', 6 'access_token_table' => 'oauth_access_tokens', 7 'refresh_token_table' => 'oauth_refresh_tokens', 8 'code_table' => 'oauth_authorization_codes', 9 'user_table' => 'oauth_users', 10 'jwt_table' => 'oauth_jwt', 11 'jti_table' => 'oauth_jti', 12 'scope_table' => 'oauth_scopes', 13 'public_key_table' => 'oauth_public_keys', 14 ), $config);
配置
我们来建立一个 server.php 文件来配置server,这个文件可以被所有的终端来调用。看require once就知道这个文件是平级的
1 <?php 2 $dsn = 'mysql:dbname=test;host=localhost'; 3 $username = 'root'; 4 $password = 'orbit'; 5 6 // error reporting (this is a demo, after all!) 7 ini_set('display_errors', 1); 8 error_reporting(E_ALL); 9 10 // Autoloading (composer is preferred, but for this example let's just do this) 11 require_once('oauth2-server-php/src/OAuth2/Autoloader.php'); 12 OAuth2Autoloader::register(); 13 14 // $dsn is the Data Source Name for your database, for exmaple "mysql:dbname=my_oauth2_db;host=localhost" 15 $storage = new OAuth2StoragePdo(array('dsn' => $dsn, 'username' => $username, 'password' => $password)); 16 17 // Pass a storage object or array of storage objects to the OAuth2 server class 18 //$server = new OAuth2Server($storage); 19 $server = new OAuth2Server($storage, array( 20 'allow_implicit' => true, 21 'refresh_token_lifetime'=> 2419200, 22 )); 23 // Add the "Client Credentials" grant type (it is the simplest of the grant types) 24 $server->addGrantType(new OAuth2GrantTypeClientCredentials($storage)); 25 26 // Add the "Authorization Code" grant type (this is where the oauth magic happens) 27 $server->addGrantType(new OAuth2GrantTypeAuthorizationCode($storage)); 28 //Resource Owner Password Credentials (资源所有者密码凭证许可) 29 $server->addGrantType(new OAuth2GrantTypeUserCredentials($storage)); 30 //can RefreshToken set always_issue_new_refresh_token=true 31 $server->addGrantType(new OAuth2GrantTypeRefreshToken($storage, array( 32 'always_issue_new_refresh_token' => true 33 ))); 34 // configure your available scopes 35 $defaultScope = 'basic'; 36 $supportedScopes = array( 37 'basic', 38 'postonwall', 39 'accessphonenumber' 40 ); 41 $memory = new OAuth2StorageMemory(array( 42 'default_scope' => $defaultScope, 43 'supported_scopes' => $supportedScopes 44 )); 45 $scopeUtil = new OAuth2Scope($memory); 46 $server->setScopeUtil($scopeUtil);
Token控制器
下面,我们将建立一个Token控制器,这个控制器URI将会返回OAuth2的Token给客户端
<?php require_once __DIR__.'/server.php'; // Handle a request for an OAuth2.0 Access Token and send the response to the client $server->handleTokenRequest(OAuth2Request::createFromGlobals())->send();
测试Token控制器
Client Credentials Grant (客户端凭证许可)
curl -u testclient:testpass http://localhost/token.php -d 'grant_type=client_credentials'
如果运行正常,则显示
{"access_token":"03807cb390319329bdf6c777d4dfae9c0d3b3c35","expires_in":3600,"token_type":"bearer","scope":null}
资源控制器的建立和测试
你创建了Token,你需要在API中测试它,于是你写了如下代码
<?php // include our OAuth2 Server object require_once __DIR__ . '/server.php'; // Handle a request for an OAuth2.0 Access Token and send the response to the client if (!$server->verifyResourceRequest(OAuth2Request::createFromGlobals())) { $server->getResponse()->send(); die; } echo json_encode(array('success' => true, 'message' => 'You accessed my APIs!'));
然后运行下面的命令,记得将YOUR_TOKEN替换成刚才得到的token,还有确保URL的正确
curl http://localhost/resource.php -d 'access_token=YOUR_TOKEN'
如果没出问题,则会得到下面的结果
{"success":true,"message":"You accessed my APIs!"}
Authorization Code Grant (授权码认证)
Authorize.php代码
1 <?php 2 // include our OAuth2 Server object 3 require_once __DIR__ . '/server.php'; 4 5 $request = OAuth2Request::createFromGlobals(); 6 $response = new OAuth2Response(); 7 8 // validate the authorize request 9 if (!$server->validateAuthorizeRequest($request, $response)) { 10 $response->send(); 11 die; 12 } 13 // display an authorization form 14 if (empty($_POST)) { 15 exit(' 16 <form method="post"> 17 <label>Do You Authorize TestClient?</label><br /> 18 <input type="submit" name="authorized" value="yes"> 19 </form>'); 20 } 21 22 // print the authorization code if the user has authorized your client 23 $is_authorized = ($_POST['authorized'] === 'yes'); 24 $server->handleAuthorizeRequest($request, $response, $is_authorized); 25 if ($is_authorized) { 26 // this is only here so that you get to see your code in the cURL request. Otherwise, we'd redirect back to the client 27 $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=') + 5, 40); 28 //exit("SUCCESS AND DO redirect_uri! Authorization Code: $code"); 29 } 30 $response->send();
然后在浏览器中打开这个URL
http://localhost/authorize.php?response_type=code&client_id=testclient&state=xyz
你将会看到一个表单,当你选择yes的时候会弹出你所获得的Authorization Code现在你可以用这个Authorization Code来刚才建立的token.php获得TOKEN,命令如下
curl -u testclient:testpass http://localhost/token.php -d 'grant_type=authorization_code&code=YOUR_CODE'
就像刚才一样,你获得了一个TOKEN
{"access_token":"6ec6afa960587133d435d67d31e8ac08efda65ff","expires_in":3600,"token_type":"Bearer","scope":null,"refresh_token":"e57fafaa693a998b302ce9ec82d940d7325748d3"}
请在30秒内完成这个操作,因为AuthorizationCode的有效期只有30秒,可以修改 OAuth2/ResponseType/AuthorizationCode.php 中的 AuthorizationCode class 的构造方法配置参数来自定义 authorization_code 有效时间.
access_token 有效期为3600s, refresh_token 有效期为 1209600s,可以在OAuth2/ResponseType/AccessToken.php 中的 AccessToken class 中的构造函数配置中进行修改。
可修改 OAuth2/GrantType/RefreshToken.php 中的 RefreshToken class __construct 方法中的 'always_issue_new_refresh_token' => true 来开启颁发新的 refresh_token.使用 refresh_token 换取 access_token:首先,刷新令牌必须使用授权码或资源所有者密码凭证许可类型检索:
curl -u testclient:testpass http:://localhost/token.php -d 'grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN'
资源所有者密码凭证许可: user 表设计使用 sha1 摘要方式,没有添加 salt.
在 Pdo.php中有protected function checkPassword($user, $password)
curl -u testclient:testpass http://localhost/token.php -d 'grant_type=password&username=rereadyou&password=rereadyou'
用Access Token联系本地用户
当你认证了一个用户并且分派了一个Token之后,你可能想知道彼时到底是哪个用户使用了这个Token
你可以使用handleAuthorizeRequest的可选参数user_id来完成,修改你的authorize.php文件
1 $userid = 1; // A value on your server that identifies the user 2 $server->handleAuthorizeRequest($request, $response, $is_authorized, $userid);
这样一来,用户ID就伴随Token一起存进数据库了当Token被客户端使用的时候,你就知道是哪个用户了,修改resource.php来完成任务
1 if (!$server->verifyResourceRequest(OAuth2Request::createFromGlobals())) { 2 $server->getResponse()->send(); 3 die; 4 } 5 $token = $server->getAccessTokenData(OAuth2Request::createFromGlobals()); 6 echo "User ID associated with this token is {$token['user_id']}";
scope 需要服务端确定具体的可行操作。
curl -u testclient:testpass http://localhost/token.php -d 'grant_type=client_credentials&scope=postonwall'
scope 用来确定 client 所能进行的操作权限。项目中操作权限由 srbac 进行控制, Oauth2 中暂不做处理
1 <?php 2 // include our OAuth2 Server object 3 require_once __DIR__ . '/server.php'; 4 5 $request = OAuth2Request::createFromGlobals(); 6 $response = new OAuth2Response(); 7 $scopeRequired = 'postonwall'; // this resource requires "postonwall" scope 8 if (!$server->verifyResourceRequest($request, $response, $scopeRequired)) { 9 // if the scope required is different from what the token allows, this will send a "401 insufficient_scope" error 10 $server->getResponse()->send(); 11 die; 12 } 13 echo json_encode(array('success' => true, 'message' => 'You accessed my APIs!'));
state 为 client app 在第一步骤中获取 authorization code 时向 OAuth2 Server 传递并由 OAuth2 Server 返回的随机哈希参数。state 参数主要用来防止跨站点请求伪造.
如果对整个调用请求中的参数进行排序,再以随机字符串nonce_str和timestamp加上排序后的参数来对整个调用生成1个sign,黑客即使截获sign,不同的时间点、参数请求所使用的sign也是不同的,难以伪造,自然会更安全。当然,写起来也更费事。加入随机字符串nonce_str主要保证签名不可预测。
1 $sign = new SignGenerator($params); 2 $sign->onSortAfter(function($that) use($key) { 3 $that->key = $key; 4 }); 5 $params['sign'] = $sign->getResult();