PHP下的Oauth2.0嘗試 - 受權碼受權(Authorization Code Grant)

OAuth 2.0

不太熟悉什麼是OAuth2.0的同窗能夠參考阮大神的文章, 理解OAuth 2.0 - 阮一峯php

受權碼模式(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 角色html

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 角色mysql

User Agent 角色,常規通常基於瀏覽器git

// 受權頁面和受權
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 角色github

// 生成並獲取token
public function token()
{
    $server = $this->server();
    $server->handleTokenRequest(\OAuth2\Request::createFromGlobals())->send();
    exit();
}

受權頁面

CLIENT 客戶端 角色ajax

# 瀏覽器訪問:
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

clipboard.png

受權頁面說明

# 咱們換行分解下
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 客戶端 角色sql

// 客戶端回調,來自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']);
}

clipboard.png

刷新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']);
}

clipboard.png

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模擬測試

正確的access_token請求:
clipboard.png

錯誤或失效的access_token請求:
clipboard.png

總結

Oauth2.0總體沒什麼具體的技術含量,能夠參照規範實現便可

後續

PHP下的Oauth2.0嘗試 - OpenID Connect - 後續補位

附錄

Oauth2.0 - Authorization Code Grant
使用Authorization_Code獲取Access_Token - QQ互聯接入
推薦閱讀登陸受權方案 - 網站的無密碼登陸 - 阮一峯

相關文章
相關標籤/搜索