OAuth2.0

資源鏈接

官方文檔
官方GITHUB
本文GITHUB DEMO

術(shù)語

  1. Resource owner 資源擁有者,比如微信用戶,擁有頭像,手機號,微信唯一標識等資源,可以授權(quán)給第三方應(yīng)用程序權(quán)限
  2. Client 第三方應(yīng)用程序,比如服務(wù)號開發(fā)商
  3. Authorization server 授權(quán)服務(wù)器,比如微信官方的服務(wù)器,成功鑒權(quán)之后發(fā)放Access token
  4. Resource server 資源服務(wù)器,比如頭像,手機,微信朋友關(guān)系等,使用Access token可以訪問受保護資源
  5. Access token 用于訪問受保護資源的令牌
  6. Authorization code 用戶授權(quán)Client代表他們訪問受保護資源時生成的中間令牌祟身。Client收到此令牌并將其交換為Access token
  7. Grant 獲得Access token的方式
  8. Scope 許可
  9. JWT JSON Web Token,一種token技術(shù)

要求

  • 為了防止中間人攻擊,授權(quán)服務(wù)器必須使用TLS證書
  • PHP >=7.2
  • openssl,json 擴展

安裝

  • 安裝composer包
composer require league/oauth2-server
  • 生成公私鑰
openssl genrsa -out private.key 2048
  • 從私鑰提取公鑰
openssl rsa -in private.key -pubout -out public.key
  • 也可以生成帶密碼的私鑰
openssl genrsa -aes128 -passout pass:_passphrase_ -out private.key 2048
  • 對應(yīng)的從私鑰提取公鑰
openssl rsa -in private.key -passin pass:_passphrase_ -pubout -out public.key
  • 生成對稱加密key
    用于加密Authorization Code 和 Refresh code
php -r 'echo base64_encode(random_bytes(32)), PHP_EOL;'

怎么選擇Grant

image.png

如果您授權(quán)一臺機器訪問資源并且您不需要用戶的許可來訪問所述資源静秆,您應(yīng)該選擇 Client credentials
如果您需要獲得資源所有者允許才能訪問資源,則需要判斷一下第三方用戶的類型
第三方是否有能力安全的存儲自己與用戶的憑據(jù)將取決于客戶端應(yīng)該使用哪種授權(quán)。
如果第三方是個有自己服務(wù)器的web應(yīng)用,則用Authorization code
如果第三方是個單頁應(yīng)用,或者移動APP,則用帶PKCE擴展的Authorization code
Password Grant和Implicit Grant已經(jīng)不再被官方推薦,完全可以被Authorization code取代,這里就不延伸討論

Client credentials grant

第三方發(fā)送POST請求給授權(quán)服務(wù)器

  • grant_type : client_credentials
  • client_id : 第三方ID
  • client_secret : 第三方Secret
  • scope : 權(quán)限范圍,多個以空格隔開
    授權(quán)服務(wù)器返回一下信息
  • token_type : Bearer
  • expires_in : token過期時間
  • access_token : 訪問令牌,是一個授權(quán)服務(wù)器加密過的JWT


    image.png

Authorization code grant

第一步

第三方構(gòu)建url把資源擁有者(用戶)重定向到授權(quán)服務(wù)器,并帶上以下get參數(shù)

  • response_type : code
  • client_id : 第三方ID
  • redirect_uri : 第三方回調(diào)地址(可選)
  • scope : 范圍
  • state : CSRF Token(可選),但是高度推薦

構(gòu)建url:

http://auth.cc/index/auth/authorize?response_type=code&client_id=wx123456789&redirect_uri=https%3A%2F%2Fwww.baidu.com&scope=basic&state=34

用戶打開構(gòu)建url,引導(dǎo)用戶登錄


image.png

登錄成功之后引導(dǎo)用戶授權(quán)


image.png

用戶同意授權(quán),重定向到第三方的redirect_uri并帶上以下get參數(shù)

  • code : Authorization code
  • state : 上面?zhèn)鞯膕tate,可驗證是否相同
image.png
第二步

第三方發(fā)送POST請求給授權(quán)服務(wù)器

  • grant_type : authorization_code
  • client_id : 第三方ID
  • client_secret : 第三方Secret
  • redirect_uri : 第三方回調(diào)地址
  • code : 第一步返回的Authorization code
    授權(quán)服務(wù)器返回一下信息
  • token_type : Bearer
  • expires_in : token過期時間
  • access_token : 訪問令牌,是一個授權(quán)服務(wù)器加密過的JWT
  • refresh_token : 訪問令牌過期時可刷新
image.png

Refresh token grant

第三方發(fā)送POST請求給授權(quán)服務(wù)器

  • grant_type : refresh_token
  • client_id : 第三方ID
  • client_secret : 第三方Secret
  • scope : 范圍
  • refresh_token : 刷新令牌
    授權(quán)服務(wù)器返回一下信息
  • token_type : Bearer
  • expires_in : token過期時間
  • access_token : 訪問令牌,是一個授權(quán)服務(wù)器加密過的JWT
  • refresh_token : 訪問令牌過期時可刷新
image.png

數(shù)據(jù)表DDL

//第三方服務(wù)商表
CREATE TABLE `oauth_clients`  (
  `oauth_clients_id`  int(10) unsigned NOT NULL AUTO_INCREMENT,
  `client_id`  varchar(80) NOT NULL,
  `client_secret`  varchar(80) DEFAULT NULL,
  `redirect_uri`  varchar(2000) DEFAULT NULL,
  `grant_types`  varchar(80) DEFAULT NULL,
  `scope`  varchar(4000) DEFAULT NULL,
  `user_id`  varchar(80) DEFAULT NULL,
  PRIMARY KEY (`oauth_clients_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
//scope表
CREATE TABLE `oauth_scopes` (
    `oauth_scopes_id` int(10) unsigned NOT NULL,
    `scope` varchar(80) NOT NULL,
    `is_default` tinyint(1) DEFAULT NULL,
    PRIMARY KEY (`oauth_scopes_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

核心代碼

<?php

namespace app\index\controller;

use auth2\Entities\UserEntity;
use auth2\Repositories\AuthCodeRepository;
use auth2\Repositories\RefreshTokenRepository;
use Laminas\Diactoros\ServerRequest;
use Laminas\Diactoros\ServerRequestFactory;
use League\OAuth2\Server\AuthorizationServer;
use auth2\Repositories\AccessTokenRepository;
use auth2\Repositories\ClientRepository;
use auth2\Repositories\ScopeRepository;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\ResourceServer;
use think\Controller;
use think\Db;
use think\Request;
use think\Response;
use think\Session;

class Auth extends Controller
{
    // 授權(quán)服務(wù)器
    private $authorizationServer;

    // 初始化授權(quán)服務(wù)器,裝載Repository
    public function __construct(Request $request = null)
    {
        parent::__construct($request);

        // 以下2個Repository可以自定義實現(xiàn)
        $clientRepository = new ClientRepository();
        $scopeRepository = new ScopeRepository();

        // 以下3個如果不是要自定義auth code / access token 可以不用處理
        $accessTokenRepository = new AccessTokenRepository();
        $authCodeRepository = new AuthCodeRepository();
        $refreshTokenRepository = new RefreshTokenRepository();

        // 私鑰
        $privateKey = ROOT_PATH . '/private.key';
        $encryptionKey = 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen'; // base64_encode(random_bytes(32))

        // 實例化AuthorizationServer
        $authorizationServer = new AuthorizationServer(
            $clientRepository,
            $accessTokenRepository,
            $scopeRepository,
            $privateKey,
            $encryptionKey
        );

        // 啟用 client credentials grant
        $authorizationServer->enableGrantType(
            new \League\OAuth2\Server\Grant\ClientCredentialsGrant(),
            new \DateInterval('PT2H')   // access token 有效期2個小時
        );

        // 啟用 authentication code grant
        $grant = new \League\OAuth2\Server\Grant\AuthCodeGrant(
            $authCodeRepository,
            $refreshTokenRepository,
            new \DateInterval('PT10M') // authorization codes 有效期10分鐘
        );
        $grant->setRefreshTokenTTL(new \DateInterval('P1M')); // refresh tokens 有效期1個月
        $authorizationServer->enableGrantType(
            $grant,
            new \DateInterval('PT2H')  // access token 有效期2個小時
        );

        // 啟用 Refresh token grant
        $grant = new \League\OAuth2\Server\Grant\RefreshTokenGrant($refreshTokenRepository);
        $grant->setRefreshTokenTTL(new \DateInterval('P1M')); // refresh tokens 有效期1個月
        $authorizationServer->enableGrantType(
            $grant,
            new \DateInterval('PT2H') // // access token 有效期2個小時
        );
        $this->authorizationServer = $authorizationServer;
    }

    /**
     * 引導(dǎo)用戶跳轉(zhuǎn)登錄
     */
    public function authorize()
    {
        //實例化 Psr\Http\Message\ServerRequestInterface
        $request = ServerRequestFactory::fromGlobals();
        $authRequest = $this->authorizationServer->validateAuthorizationRequest($request);
        //保存session
        Session::set('auth_request', serialize($authRequest));
        return $this->fetch('login');
    }

    /**
     * 驗證登錄
     */
    public function login(Request $request)
    {
        if (!$request->isPost()) {
            $this->error('錯誤請求');
        }
        //用戶登錄
        $user = Db::table('oauth_users')->where(['username' => $request->post('username'), 'password' => $request->post('password')])->find();
        if (empty($user)) {
            $this->error('密碼錯誤');
        }
        $authRequest = unserialize(Session::get('auth_request'));
        //設(shè)置openid
        $authRequest->setUser(new UserEntity($user['openid'])); // an instance of UserEntityInterface
        Session::set('auth_request', serialize($authRequest));
        return $this->fetch('approve');
    }

    /**
     * 引導(dǎo)用戶授權(quán)
     */
    public function approve(Request $request)
    {
        $q = $request->get();
        if (is_null($approve = $q['approve'])) {
            $this->error('錯誤請求');
        }
        $authRequest = unserialize(Session::get('auth_request'));
        $authRequest->setAuthorizationApproved((bool)$approve);
        $response = new \Laminas\Diactoros\Response();
        try {
            $psrResponse = $this->authorizationServer->completeAuthorizationRequest($authRequest, $response);
        } catch (OAuthServerException $e) {
            //用戶拒絕授權(quán),報錯
            return convertResponsePsr2Tp($e->generateHttpResponse($response));
        }
        //用戶統(tǒng)一授權(quán) 跳轉(zhuǎn)第三方redirect_uri
        return convertResponsePsr2Tp($psrResponse);
    }


    /**
     * 獲取access token
     */
    public function token(Request $request)
    {
        $request = ServerRequestFactory::fromGlobals();
        $response = new \Laminas\Diactoros\Response();
        try {
            $response = $this->authorizationServer->respondToAccessTokenRequest($request, $response);
        } catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
            return response($exception->getMessage());
        } catch (\Exception $exception) {
            return response($exception->getMessage());
        }
        return convertResponsePsr2Tp($response);
    }

    /**
     * 刷新access token
     */
    public function refresh(Request $request){
        $request = ServerRequestFactory::fromGlobals();
        $response = new \Laminas\Diactoros\Response();
        try {
            $response = $this->authorizationServer->respondToAccessTokenRequest($request, $response);
        } catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
            return response($exception->getHint());
        } catch (\Exception $exception) {
            return response($exception->getMessage());
        }
        return convertResponsePsr2Tp($response);
    }

    /**
     * 驗證access token
     */
    public function check()
    {
        $accessTokenRepository = new AccessTokenRepository(); // instance of AccessTokenRepositoryInterface
        // 初始化資源服務(wù)器
        $server = new ResourceServer(
            $accessTokenRepository,
            ROOT_PATH . '/public.key'
        );
        $request = ServerRequestFactory::fromGlobals();
        $response = new \Laminas\Diactoros\Response();
        try {
            $request = $server->validateAuthenticatedRequest($request);
        } catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
            return convertResponsePsr2Tp($exception->generateHttpResponse($response));
        } catch (\Exception $exception) {
            return convertResponsePsr2Tp((new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
                ->generateHttpResponse($response));
        }
        $attr = $request->getAttributes();
        //第三方的client_id
        $oauth_client_id = $attr['oauth_client_id'];
        //用戶的openid
        $oauth_user_id = $attr['oauth_user_id'];
        //權(quán)限
        $oauth_scopes = $attr['oauth_scopes'];

        //業(yè)務(wù)邏輯
        //...
    }
}

兩個需要自己實現(xiàn)的Repository

<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace auth2\Repositories;

use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use auth2\Entities\ClientEntity;
use think\Db;

class ClientRepository implements ClientRepositoryInterface
{
    /**
     * 返回第三方基本信息
     */
    public function getClientEntity($clientIdentifier)
    {
        //查詢數(shù)據(jù)庫
        $merchant = Db::table('oauth_clients')->where(['client_id' => $clientIdentifier])->find();
        if (empty($merchant)) {
            return false;
        }

        $client = new ClientEntity();

        $client->setIdentifier($clientIdentifier);
        $client->setName($merchant['oauth_clients_id']);
        $client->setRedirectUri($merchant['redirect_uri']);
        $client->setConfidential();

        return $client;
    }

    /**
     * 驗證第三方client_id client_secret
     */
    public function validateClient($clientIdentifier, $clientSecret, $grantType)
    {
        $client = Db::table('oauth_clients')->where(['client_id' => $clientIdentifier])->find();
        // 判斷第三方是否注冊
        if (!$client) {
            return false;
        }
        // 驗證client_secret
        if ((bool)$client['is_confidential'] === true && $clientSecret != $client['client_secret']) {
            return false;
        }
        return true;
    }
}
<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace auth2\Repositories;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use auth2\Entities\ScopeEntity;
use think\Db;

class ScopeRepository implements ScopeRepositoryInterface
{
    /**
     * 調(diào)用此方法來驗證Scope
     */
    public function getScopeEntityByIdentifier($scopeIdentifier)
    {
        $count = Db::table('oauth_scopes')->where(['scope'=>$scopeIdentifier])->count();
        if (!$count) {
            return false;
        }

        $scope = new ScopeEntity();
        $scope->setIdentifier($scopeIdentifier);

        return $scope;
    }

    /**
     * 在創(chuàng)建訪問令牌或授權(quán)代碼之前調(diào)用此方法祝峻。
     * 可以在這個方法里面修改第三方的Scope
     */
    public function finalizeScopes(
        array $scopes,
        $grantType,
        ClientEntityInterface $clientEntity,
        $userIdentifier = null
    ) {
        // 這里給第三方添加一個email權(quán)限
        if ((int) $userIdentifier === 1) {
            $scope = new ScopeEntity();
            $scope->setIdentifier('email');
            $scopes[] = $scope;
        }

        return $scopes;
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末垄提,一起剝皮案震驚了整個濱河市漩氨,隨后出現(xiàn)的幾起案子饭耳,更是在濱河造成了極大的恐慌,老刑警劉巖睛约,帶你破解...
    沈念sama閱讀 212,686評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鼎俘,死亡現(xiàn)場離奇詭異,居然都是意外死亡辩涝,警方通過查閱死者的電腦和手機贸伐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,668評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怔揩,“玉大人捉邢,你說我怎么就攤上這事∩滩玻” “怎么了伏伐?”我有些...
    開封第一講書人閱讀 158,160評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長晕拆。 經(jīng)常有香客問我藐翎,道長,這世上最難降的妖魔是什么实幕? 我笑而不...
    開封第一講書人閱讀 56,736評論 1 284
  • 正文 為了忘掉前任吝镣,我火速辦了婚禮,結(jié)果婚禮上昆庇,老公的妹妹穿的比我還像新娘末贾。我一直安慰自己,他們只是感情好整吆,可當(dāng)我...
    茶點故事閱讀 65,847評論 6 386
  • 文/花漫 我一把揭開白布拱撵。 她就那樣靜靜地躺著,像睡著了一般掂为。 火紅的嫁衣襯著肌膚如雪裕膀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,043評論 1 291
  • 那天勇哗,我揣著相機與錄音昼扛,去河邊找鬼。 笑死欲诺,一個胖子當(dāng)著我的面吹牛抄谐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播扰法,決...
    沈念sama閱讀 39,129評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼蛹含,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了塞颁?” 一聲冷哼從身側(cè)響起浦箱,我...
    開封第一講書人閱讀 37,872評論 0 268
  • 序言:老撾萬榮一對情侶失蹤吸耿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后酷窥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咽安,經(jīng)...
    沈念sama閱讀 44,318評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,645評論 2 327
  • 正文 我和宋清朗相戀三年蓬推,在試婚紗的時候發(fā)現(xiàn)自己被綠了妆棒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,777評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡沸伏,死狀恐怖糕珊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情毅糟,我是刑警寧澤红选,帶...
    沈念sama閱讀 34,470評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站留特,受9級特大地震影響纠脾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蜕青,卻給世界環(huán)境...
    茶點故事閱讀 40,126評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望糊渊。 院中可真熱鬧右核,春花似錦、人聲如沸渺绒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,861評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宗兼。三九已至躏鱼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間殷绍,已是汗流浹背染苛。 一陣腳步聲響...
    開封第一講書人閱讀 32,095評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留主到,地道東北人茶行。 一個月前我還...
    沈念sama閱讀 46,589評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像登钥,于是被迫代替她去往敵國和親畔师。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,687評論 2 351

推薦閱讀更多精彩內(nèi)容