原文轉自 http://www.cnblogs.com/ldms/p/4565547.html
Yii 有很多 extension 可以使用,在查看了 Yii 官網上提供的與 OAuth 相關的擴展後,發現了幾個 OAuth2 的客戶端擴展,但是並沒有找到可以作為 OAuth2 Server 的擴展。因為 Yii 是組織良好的易於擴展的框架,所以完全可以集成其它的 PHP OAuth2 Server 實現方案。在 OAuth.net/2/ 官網上,提供了幾個 PHP 實現的 OAuth2 Server。這裡使用第一個 OAuth2-Server-php 來作為 Yii 框架的 OAuth2 Server 擴展,需要進行一些必要的整合操作,主要是編寫一個類來接受 client 訪問和頒發 access_token 等。
第一部分: 數據庫准備
OAuth2-Server-php 使用的數據庫結構采用 Github 上的 oauth2-server-php README.md 提供的表結構(Schema),一共有五張表:
mysql> show tables;
+--------------------------+
| Tables_in_oauth2 |
+--------------------------+
| oauth_access_token |
| oauth_authorization_code |
| oauth_client |
| oauth_refresh_token |
| user |
+--------------------------+
5 rows in set (0.00 sec)
各表的名字說明了表中存取的內容,表名可自定義,自定義位置為:OAuth2/Storage/Pdo.php 48行的 config 數組中,因為這裡采用的是 mysql 數據庫,所以需要修改的是 Pdo,若是采用其它的存儲方案,如 Redis,則自行修改對應文件即可。注意這裡的數據庫名稱是都是單數形式。
使用以下 sql 語句創建這5個表,並添加一個測試 client:
###############################
### oauth2 tables
###############################
drop table if exists `oauth_client`;
drop table if exists `oauth_access_token`;
drop table if exists `oauth_authorization_code`;
drop table if exists `oauth_refresh_token`;
drop table if exists `user`;
CREATE TABLE `oauth_client` (
`client_id` VARCHAR(80) NOT NULL,
`client_secret` VARCHAR(80) NOT NULL,
`redirect_uri` VARCHAR(2000) NOT NULL,
CONSTRAINT client_id_pk PRIMARY KEY (client_id)
);
CREATE TABLE `oauth_access_token` (
`access_token` VARCHAR(40) NOT NULL,
`client_id` VARCHAR(80) NOT NULL,
`user_id` VARCHAR(255),
`expires` TIMESTAMP NOT NULL,
`scope` VARCHAR(2000),
CONSTRAINT access_token_pk PRIMARY KEY (access_token)
);
CREATE TABLE `oauth_authorization_code` (
`authorization_code` VARCHAR(40) NOT NULL,
`client_id` VARCHAR(80) NOT NULL,
`user_id` VARCHAR(255),
`redirect_uri` VARCHAR(2000),
`expires` TIMESTAMP NOT NULL,
`scope` VARCHAR(2000),
CONSTRAINT auth_code_pk PRIMARY KEY (authorization_code)
);
CREATE TABLE `oauth_refresh_token` (
`refresh_token` VARCHAR(40) NOT NULL,
`client_id` VARCHAR(80) NOT NULL,
`user_id` VARCHAR(255),
`expires` TIMESTAMP NOT NULL,
`scope` VARCHAR(2000),
CONSTRAINT refresh_token_pk PRIMARY KEY (refresh_token)
);
--
CREATE TABLE `user` (
`user_id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(2000),
`first_name` VARCHAR(255),
`last_name` VARCHAR(255),
CONSTRAINT user_pk PRIMARY KEY (user_id)
);
-- test data
INSERT INTO oauth_client (client_id, client_secret, redirect_uri)
VALUES ("testclient", "testpass", "http://fake/");
INSERT INTO user (username, password, first_name, last_name)
VALUES ('rereadyou', '8551be07bab21f3933e8177538d411e43b78dbcc', 'bo', 'zhang');
第二部分: 認證方案及實現
OAuth2 RFC 6749 規范提供了四種基本認證方案,以下針對這四種認證方案以及它們在本實現中的使用方式進行分別說面。
第一種認證方式: Authorization Code Grant (授權碼認證)
授權碼通過使用授權服務器做為客戶端與資源所有者的中介而獲得。客戶端不是直接從資源所有者請求授權,而是引導資源所有者至授權服務器(由在RFC2616中定義的用戶代理),授權服務器之後引導資源所有者帶著授權碼回到客戶端。
在引導資源所有者攜帶授權碼返回客戶端前,授權服務器會鑒定資源所有者身份並獲得其授權。由於資源所有者只與授權服務器進行身份驗證,所以資源所有者的憑據不需要與客戶端分享。
授權碼提供了一些重要的安全益處,例如驗證客戶端身份的能力,以及向客戶端直接的訪問令牌的傳輸而非通過資源所有者的用戶代理來傳送它而潛在暴露給他人(包括資源所有者)。
授權碼許可類型用於獲得訪問令牌和刷新令牌並未機密客戶端進行了優化。由於這是一個基於重定向的流程,客戶端必須能夠與資源所有者的用戶代理(通常是Web浏覽器)進行交互並能夠接收來自授權服務器的傳入請求(通過重定向)。
Authorization Code Grant 過程(又稱為 Web Server Flow) 參見如下:
+----------+
| 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)
注:說明步驟(A)、(B)和(C)的直線因為通過用戶代理而被分為兩部分。
圖1:授權碼流程
在圖1中所示的流程包括以下步驟:
(A)客戶端通過向授權端點引導資源所有者的用戶代理開始流程。客戶端包括它的客戶端標識、請求范圍、本地狀態和重定向URI,一旦訪問被許可(或拒絕)授權服務器將傳送用戶代理回到該URI。
(B)授權服務器驗證資源擁有者的身份(通過用戶代理),並確定資源所有者是否授予或拒絕客戶端的訪問請求。
(C)假設資源所有者許可訪問,授權服務器使用之前(在請求時或客戶端注冊時)提供的重定向URI重定向用戶代理回到客戶端。重定向URI包括授權碼和之前客戶端提供的任何本地狀態。
(D)客戶端通過包含上一步中收到的授權碼從授權服務器的令牌端點請求訪問令牌。當發起請求時,客戶端與授權服務器進行身份驗證。客戶端包含用於獲得授權碼的重定向URI來用於驗證。
(E)授權服務器對客戶端進行身份驗證,驗證授權代碼,並確保接收的重定向URI與在步驟(C)中用於重定向客戶端的URI相匹配。如果通過,授權服務器響應返回訪問令牌與可選的刷新令牌。
過程實現:
1. client app 使用 app id 獲取 authorization code:
www.yii.com/oauth2/index.php?r=oauth2/authroize&response_type=code&client_id=testclient&state=xyz
返回:$authcode = authorization code.
Tips: authorization code will expired in 30s,可以修改 OAuth2/ResponseType/AuthorizationCode.php 中的 AuthorizationCode class 的構造方法配置參數來自定義 authorization_code 有效時間。
client_id 是之前注冊在本 Server 上的應用名稱,這屬於客戶端管理范疇。
這一步需要進行用戶(資源所有者)登錄 OAuth2 Server 來完成授權操作。用戶登錄屬用戶管理范疇,不屬 OAuth2 Server 中應編寫的功能。
用戶登錄後可選擇自己可以向 client app 開放的操作(授權)。
這一步綁定過程中,從安全角度來考慮應強制用戶重新輸入用戶名密碼確認綁定,不要直接讀取當前用戶session進行綁定。
2. 獲取 access_token:
client app 使用 authorization code 換取 access_token
curl -u testclient:testpass www.yii.com/oauth2/index.php?r=oauth2/token -d "grant_type=authorization_code&code=$authcode
返回:
成功:
{"access_token":"aea4a1059d3194a3dd5e4117bedd6e07ccc3f402",
"expires_in":3600,
"token_type":"bearer",
"scope":null,
"refresh_token":"269a623f54171e8598b1852eefcf115f4882b820"
}
失敗:
{"error":"invalid_grant",
"error_description":"Authorization code doesn't exist or is invalid for the client"
}
Tip: 本步驟需要使用客戶端的 client_id 和 client_secret 以及上一步獲取的 authorization_code 換取 access_code.
access_tokne 有效期為 3600s, refresh_token 有效期為 1209600s,可以在 OAuth2/ResponseType/AccessToken.php 中的 AccessToken class 中的構造函數配置中進行修改。
第二種認證方式: Implicit (隱式認證)
隱式授權類型被用於獲取訪問令牌(它不支持發行刷新令牌),並對知道操作具體重定向URI的公共客戶端進行優化。這些客戶端通常在浏覽器中使用諸如JavaScript的腳本語言實現。
由於這是一個基於重定向的流程,客戶端必須能夠與資源所有者的用戶代理(通常是Web浏覽器)進行交互並能夠接收來自授權服務器的傳入請求(通過重定向)。
不同於客戶端分別請求授權和訪問令牌的授權碼許可類型,客戶端收到訪問令牌作為授權請求的結果。
隱式許可類型不包含客戶端身份驗證而依賴於資源所有者在場和重定向URI的注冊。因為訪問令牌被編碼到重定向URI中,它可能會暴露給資源所有者和其他駐留在相同設備上的應用。
采用Implicit Grant方式獲取Access Token的授權驗證流程又被稱為User-Agent Flow,適用於所有無Server端配合的應用(由於應用往往位於一個User Agent裡,如浏覽器裡面,因此這類應用在某些平台下又被稱為Client-Side Application),如手機/桌面客戶端程序、浏覽器插件等,以及基於JavaScript等腳本客戶端腳本語言實現的應用,他們的一個共同特點 是,應用無法妥善保管其應用密鑰(App Secret Key),如果采取Authorization Code模式,則會存在洩漏其應用密鑰的可能性。其流程示意圖如下:
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| +----(A)-- & Redirection URI --->| |
| User- | | Authorization |
| Agent |----(B)-- User authenticates -->| Server |
| | | |
| |<---(C)--- Redirection URI ----<| |
| | with Access Token +---------------+
| | in Fragment
| | +---------------+
| |----(D)--- Redirection URI ---->| Web-Hosted |
| | without Fragment | Client |
| | | Resource |
| (F) |<---(E)------- Script ---------<| |
| | +---------------+
+-|--------+
| |
(A) (G) Access Token
| |
^ v
+---------+
| |
| Client |
| |
+---------+
注:說明步驟(A)和(B)的直線因為通過用戶代理而被分為兩部分。
圖2:隱式許可流程
圖2中的所示流程包含以下步驟:
(A)客戶端通過向授權端點引導資源所有者的用戶代理開始流程。客戶端包括它的客戶端標識、請求范圍、本地狀態和重定向URI,一旦訪問被許可(或拒絕)授權服務器將傳送用戶代理回到該URI。
(B)授權服務器驗證資源擁有者的身份(通過用戶代理),並確定資源所有者是否授予或拒絕客戶端的訪問請求。
(C)假設資源所有者許可訪問,授權服務器使用之前(在請求時或客戶端注冊時)提供的重定向URI重定向用戶代理回到客戶端。重定向URI在URI片段中包含訪問令牌。
(D)用戶代理順著重定向指示向Web托管的客戶端資源發起請求(按RFC2616該請求不包含片段)。用戶代理在本地保留片段信息。
(E)Web托管的客戶端資源返回一個網頁(通常是帶有嵌入式腳本的HTML文檔),該網頁能夠訪問包含用戶代理保留的片段的完整重定向URI並提取包含在片段中的訪問令牌(和其他參數)。
(F)用戶代理在本地執行Web托管的客戶端資源提供的提取訪問令牌的腳本。
(G)用戶代理傳送訪問令牌給客戶端。
Tips: 1. 一般不需提供 client_secret,僅需 client_id,單用戶同樣需要認證。
2. Implicit Grant Type 不支持 refresh_token(或可自行實現)機制。
3. THE FIRST TIME THE USER AUTHENTICATES YOUR APP USING IMPLICIT GRANT FLOW STORE THE ACCESS TOKEN! Once you have the access token do not try to re-authenticate. Your access token that you stored should continue to work!
一旦獲取 access_token (存在於 redirect_uri 的 fragment 中, 即 uri 中的 # 部分),Client 需要自己存儲 access_token。
4. 比較適用於 Client-Side Application,如手機/桌面客戶端程序、浏覽器插件等
oauth2-server-php 對本授權方式的實現如下:
1. 這種授權方式包含於 Authorization Code Grant (是對 Authorization Code Grant 方式的簡化)。
初始化 OAuth2Controller 時, 只需向 OAuth2 Server 添加 AuthorizationCode 類型的授權即可,如下:
$server->addGrantType(new OAuth2\GrantType\AuthorizationCode($storage));
Authorization Code 默認不支持 Implicit Grant, 需要將 Server.php 第 104 行的 'allow_implicit' 修改為 'true' 以開啟 Implicit 授權。
2. 獲取 access_token
http://www.yii.com/oauth2/index.php?r=oauth2/authorize&response_type=token&client_id=testclient&state=xyz&redirect_uri=www.baidu.com
參數: response_type=token (必須, 固定值)
client_id (必須)
redirect_uri 可選
scope 可選
state 推薦
注意:response_type = token 而不是 code, 因為隱式授權不用獲取 authorization code。
返回:
成功:
需要用戶先點擊授權按鈕。
SUCCESS! Authorization Code: www.baidu.com?#access_token=9f0c38b475e51ccd3
出錯: redirect_uri 與注冊的 client redirect_uri 不匹配。
{"error":"redirect_uri_mismatch","error_description":"The redirect URI provided is missing or does not match","error_uri":"http:\/\/tools.ietf.org\/html\/rfc6749#section-3.1.2"}
access_token 存在於 redirect_uri 中的片段(fragment)中, 即‘#’符號之後,client 需要自己提取片段中的 access_token 並注意保存。開發人員應注意,一些用戶代理不支持在HTTP“Location”HTTP響應標頭字段中包含片段組成部分。這些客戶端需要使用除了3xx 重定向響應以外的其他方法來重定向客戶端——-例如,返回一個HTML頁面,其中包含一個具有鏈接到重定向URI的動作的“繼續”按鈕。
第三種認證方式: Resource Owner Password Credentials (資源所有者密碼憑證許可)
資源所有者密碼憑據許可類型適合於資源所有者與客戶端具有信任關系的情況,如設備操作系統或高級特權應用。當啟用這種許可類型時授權服務器應該特別關照且只有當其他流程都不可用時才可以。
這種許可類型適合於能夠獲得資源所有者憑據(用戶名和密碼,通常使用交互的形式)的客戶端。通過轉換已存儲的憑據至訪問令牌,它也用於遷移現存的使用如HTTP基本或摘要身份驗證的直接身份驗證方案的客戶端至OAuth。
+----------+
| Resource |
| Owner |
| |
+----------+
v
| Resource Owner
(A) Password Credentials
|
v
+---------+ +---------------+
| |>--(B)---- Resource Owner ------->| |
| | Password Credentials | Authorization |
| Client | | Server |
| |<--(C)---- Access Token ---------<| |
| | (w/ Optional Refresh Token) | |
+---------+ +---------------+
圖3:資源所有者密碼憑據流程
圖3中的所示流程包含以下步驟:
(A)資源所有者提供給客戶端它的用戶名和密碼。
(B)通過包含從資源所有者處接收到的憑據,客戶端從授權服務器的令牌端點請求訪問令牌。當發起請求時,客戶端與授權服務器進行身份驗證。
(C)授權服務器對客戶端進行身份驗證,驗證資源所有者的憑證,如果有效,頒發訪問令牌。
Tips: 客戶端一旦獲得訪問令牌必須丟棄憑據。
oauth2-server-php 對 Resource Owner Password Credentials 的實現如下:
1. 首先在 Oauth2Controller 的構造函數中添加對於 Resource Owner Password Credentials 授權方式的支持,加入以下代碼:
$server->addGrantType(new OAuth2\GrantType\UserCredentials($storage));
2. 獲取 access_token :
curl -u testclient:testpass www.yii.com/oauth2/index.php?r=oauth2/token -d 'grant_type=password&username=rereadyou&password=rereadyou'
返回:
{"access_token":"66decd1b10891db5f8f63efe7cc352ce326895c6",
"expires_in":3600,
"token_type":"bearer",
"scope":null,
"refresh_token":"b5fa0c24e786e37e7ce7d6e2f911805dc65a0d7c"}
Tips: Github 上 oauth2-server-php 提供的 sql schema user 表裡面沒有 user_id 字段[12],需要自行添加該字段(主鍵, auto_increment)。
user 表設計使用 sha1 摘要方式,沒有添加 salt。
在 Pdo.php 中有:
// plaintext passwords are bad! Override this for your application
protected function checkPassword($user, $password)
{
return $user['password'] == sha1($password);
}
對於用戶認證需要改寫這個函數。
第四種認證方式: Client Credentials Grant (客戶端憑證許可)
當客戶端請求訪問它所控制的,或者事先與授權服務器協商(所采用的方法超出了本規范的范圍)的其他資源所有者的受保護資源,客戶端可以只使用它的客戶端憑據(或者其他受支持的身份驗證方法)請求訪問令牌。
客戶端憑據許可類型必須只能由機密客戶端使用。
+---------+ +---------------+
| | | |
| |>--(A)- Client Authentication --->| Authorization |
| Client | | Server |
| |<--(B)---- Access Token ---------<| |
| | | |
+---------+ +---------------+
圖4:客戶端憑證流程
圖4中的所示流程包含以下步驟:
(A)客戶端與授權服務器進行身份驗證並向令牌端點請求訪問令牌。
(B)授權服務器對客戶端進行身份驗證,如果有效,頒發訪問令牌。
Tips: 這是最簡單的認證方式。
由於客戶端身份驗證被用作授權許可,所以不需要其他授權請求。
實現如下:
1. 在 Oauth2Controller 中添加對 client credentials 認證方式的支持:
$server->addGrantType(new OAuth2\GrantType\ClientCredentials($storage));
2. 獲取 access_token:
curl -u testclient:testpass www.yii.com/oauth2/index.php?r=oauth2/token -d 'grant_type=client_credentials'
提交參數: grant_type REQUIRED. Value MUST be set to "client_credentials".
scope OPTIONAL.
返回:
{"access_token": "f3c30def0d28c633e34921b65388eb0bbd9d5ff9",
"expires_in":3600,
"token_type":"bearer",
"scope":null}
Tips: Client 直接使用自己的 client id 和 client_secret 獲取 access_token;
RFC6749規范指明[10] clinet crendentials 客戶端認證取得 access_token 時不包括 refresh_token。
不過,oauth2-server-php 提供了控制開關,在 OAuth2/GrantTypes/ClientCredentials.php 第 33 行[11],
默認 $includeRefreshToken = false; 設置為 true, 則可在頒發 access_token 同時頒發 refresh_token。
第三部分: access_token 類型說明
客戶端在操作數據資源時(通過 api)需要向 server 出示 access_token,關於如何出示 access_token 和 access_token 類型由以下部分說明。
IETF rfc 6749 中說明的 access_token 類型有兩種:Bearer type 和 MAC type。
由於 OAuth2-Server-php 對於 MAC 類型的 access_token 尚在開發之中,以下僅對最常使用的 Bearer 類型 access_token 進行說明。
有三種在資源請求中發送 bearer access_token 資源給資源服務器的方法[13]。客戶端不能在每次請求中使用超過一個方法傳輸令牌。
a. 當在由HTTP/1.1[RFC2617]定義的“Authorization”請求頭部字段中發送訪問令牌時,客戶端使用“Bearer”身份驗證方案來傳輸訪問令牌。
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM
客戶端應該使用帶有“Bearer”HTTP授權方案的“Authorization”請求頭部字段發起帶有不記名令牌的身份驗證請求。資源服務器必須支持此方法。
b. 表單編碼的主體參數
當在HTTP請求實體主體中發送訪問令牌時,客戶端采用“access_token”參數向請求主體中添加訪問令牌。客戶端不能使用此方法,除非符合下列所有條件:
HTTP請求的實體頭部含有設置為“application/x-www-form-urlencoded”的“Content-Type”頭部字段。
實體主體遵循HTML4.01[W3C.REC-html401-19991224]定義的“application/x-www-form-urlencoded”內容類型的編碼要求。
HTTP請求實體主體是單一的部分。
在實體主體中編碼的內容必須完全由ASCII[USASCII]字符組成。
HTTP請求方法是請求主體定義為其定義的語法。尤其是,這意味著“GET”方法不能被使用。
客戶端采用傳輸層安全發起如下的HTTP請求:
POST /resource HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
access_token=mF_9.B5f-4.1JqM
c. 當在HTTP請求URI中發送訪問令牌時,客戶端采用“access_token”參數,向“統一資源標示符(URI):通用語法”RFC3986定義的請求URI查詢部分添加訪問令牌。
例如,客戶端采用傳輸層安全發起如下的HTTP請求:
GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
Host: server.example.com
它不應該被使用,除非不能在“Authorization”請求頭部字段或HTTP請求實體主體中傳輸訪問令牌。
以上在 rfc6750 規范中提出的三種 access_token 的使用方式。推薦使用第一種方案。Bearer token 的使用需要借助 TLS 來確保 access_token 傳輸時的安全性。
第四部分: 使用 Bearer access_token 的調用 api
1. 使用 refresh_token 換取 access_token:
curl -u testclient:testpass www.yii.com/oauth2/index.php?r=oauth2/token -d "grant_type=refresh_token&refresh_token=1ce1a52dff3b5ab836ae25714c714cb86bf31b6f"
返回:
{"access_token":"50540a7ead3a27cdb458b6cdc38df25f64da18f1",
"expires_in":3600,
"token_type":"bearer",
"scope":null}
這裡沒有新的 refresh_token,需要進行配置以重新獲取 refresh_token,可修改 OAuth2/GrantType/RefreshToken.php 中的 RefreshToken class __construct 方法中的 'always_issue_new_refresh_token' => true 來開啟頒發新的 refresh_token。
Tips: IETF rfc2649 中對於 refresh_token section 的部分說明,
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
需要提供客戶端的 client_id 和 client_secret, grant_type 值必須是 refresh_token。
access_token 有效期內不能使用 refresh_token 換取新的 access_token。
2. 使用 access_token:
a. client app 使用 access_token 獲取 resource 信息。
oauth2-server 驗證 access_token:
curl www.yii.com/oauth2/index.php?r=oauth2/verifytoken -d 'access_token=aea4a1059d3194a3dd5e4117bedd6e07ccc3f402'
返回:
{"result":"success",
"message":"your access token is valid."
} 這個部分只是為了驗證 access token 的有效性,client app 並不應該直接調用該方法,而是在請求資源時有server自行調用,根據判斷結果進行不同處理。
可以在 Oauth2 extension 的 Server.php 中來修改 access_token 的有效期。
3. scope
scope 需要服務端確定具體的可行操作。
scope 用來確定 client 所能進行的操作權限。項目中操作權限由 srbac 進行控制, Oauth2 中暫不做處理。
4. state
state 為 client app 在第一步驟中獲取 authorization code 時向 OAuth2 Server 傳遞並由 OAuth2 Server 返回的隨機哈希參數。state 參數主要用來防止跨站點請求偽造(Cross Site Request Forgery, CSRF),相關討論可參見本文最後的參考【7】和【8】。
References: