API介面設計 OAuth2.0認證

航空母艦發表於2016-04-05

OAuth2.0官網

git clone https://github.com/bshaffer/oauth2-server-php.git

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

CREATE TABLE `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,
PRIMARY KEY (`access_token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `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,
PRIMARY KEY (`authorization_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `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,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `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,
PRIMARY KEY (`refresh_token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_users` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(2000) DEFAULT NULL,
`first_name` varchar(255) DEFAULT NULL,
`last_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

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

CREATE TABLE `oauth_jwt` (
  `client_id` varchar(80) NOT NULL,
  `subject` varchar(80) DEFAULT NULL,
  `public_key` varchar(2000) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- test data
INSERT INTO oauth_clients (client_id, client_secret, redirect_uri) VALUES ("testclient", "testpass", "http://www.baidu.com/");
INSERT INTO oauth_users (username, password, first_name, last_name) VALUES ('rereadyou', '8551be07bab21f3933e8177538d411e43b78dbcc', 'bo', 'zhang');

各表的名字說明了表中存取的內容,表名可自定義,自定義位置為:OAuth2/Storage/Pdo.php 48行的 config 陣列中,因為這裡採用的是 mysql 資料庫,所以需要修改的是 Pdo,若是採用其它的儲存方案,如 Redis,則自行修改對應檔案即可。注意這裡的資料庫名稱是都是單數形式

配置

我們來建立一個server.php檔案來配置server,這個檔案可以被所有的終端來呼叫。看require once就知道這個檔案是平級的

<?php
$dsn = 'mysql:dbname=test;host=localhost';
$username = 'root';
$password = 'orbit';

// error reporting (this is a demo, after all!)
ini_set('display_errors', 1);
error_reporting(E_ALL);

// Autoloading (composer is preferred, but for this example let's just do this)
require_once('oauth2-server-php/src/OAuth2/Autoloader.php');
OAuth2\Autoloader::register();

// $dsn is the Data Source Name for your database, for exmaple "mysql:dbname=my_oauth2_db;host=localhost"
$storage = new OAuth2\Storage\Pdo(array('dsn' => $dsn, 'username' => $username, 'password' => $password));

// Pass a storage object or array of storage objects to the OAuth2 server class
//$server = new OAuth2\Server($storage);
$server = new OAuth2\Server($storage, array(
    'allow_implicit' => true,
    'refresh_token_lifetime'=> 2419200,
));
// Add the "Client Credentials" grant type (it is the simplest of the grant types)
$server->addGrantType(new OAuth2\GrantType\ClientCredentials($storage));

// Add the "Authorization Code" grant type (this is where the oauth magic happens)
$server->addGrantType(new OAuth2\GrantType\AuthorizationCode($storage));
//Resource Owner Password Credentials (資源所有者密碼憑證許可)
$server->addGrantType(new OAuth2\GrantType\UserCredentials($storage));
//can RefreshToken set always_issue_new_refresh_token=true
$server->addGrantType(new OAuth2\GrantType\RefreshToken($storage, array(
    'always_issue_new_refresh_token' => true
)));
// configure your available scopes
$defaultScope = 'basic';
$supportedScopes = array(
    'basic',
    'postonwall',
    'accessphonenumber'
);
$memory = new OAuth2\Storage\Memory(array(
    'default_scope' => $defaultScope,
    'supported_scopes' => $supportedScopes
));
$scopeUtil = new OAuth2\Scope($memory);
$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(OAuth2\Request::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(OAuth2\Request::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 (授權碼認證)

<?php
// include our OAuth2 Server object
require_once __DIR__ . '/server.php';

$request = OAuth2\Request::createFromGlobals();
$response = new OAuth2\Response();

// validate the authorize request
if (!$server->validateAuthorizeRequest($request, $response)) {
    $response->send();
    die;
}
// display an authorization form
if (empty($_POST)) {
    exit('
<form method="post">
  <label>Do You Authorize TestClient?</label><br />
  <input type="submit" name="authorized" value="yes">
</form>');
}

// print the authorization code if the user has authorized your client
$is_authorized = ($_POST['authorized'] === 'yes');
$server->handleAuthorizeRequest($request, $response, $is_authorized);
if ($is_authorized) {
    // this is only here so that you get to see your code in the cURL request. Otherwise, we'd redirect back to the client
    $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=') + 5, 40);
    //exit("SUCCESS AND DO redirect_uri! Authorization Code: $code");
}
$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檔案

$userid = 1; // A value on your server that identifies the user
$server->handleAuthorizeRequest($request, $response, $is_authorized, $userid);

這樣一來,使用者ID就伴隨Token一起存進資料庫了當Token被客戶端使用的時候,你就知道是哪個使用者了,修改resource.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']}";

scope 需要服務端確定具體的可行操作。

curl -u testclient:testpass http://localhost/token.php -d 'grant_type=client_credentials&scope=postonwall'

scope 用來確定 client 所能進行的操作許可權。專案中操作許可權由 srbac 進行控制, Oauth2 中暫不做處理

<?php
// include our OAuth2 Server object
require_once __DIR__ . '/server.php';

$request = OAuth2\Request::createFromGlobals();
$response = new OAuth2\Response();
$scopeRequired = 'postonwall'; // this resource requires "postonwall" scope
if (!$server->verifyResourceRequest($request, $response, $scopeRequired)) {
    // if the scope required is different from what the token allows, this will send a "401 insufficient_scope" error
    $server->getResponse()->send();
    die;
}
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主要保證簽名不可預測

$sign = new SignGenerator($params);
$sign->onSortAfter(function($that) use($key) {
    $that->key = $key;
});
$params['sign'] = $sign->getResult();

 

 

相關文章