資源鏈接
術(shù)語
- Resource owner 資源擁有者,比如微信用戶,擁有頭像,手機號,微信唯一標識等資源,可以授權(quán)給第三方應(yīng)用程序權(quán)限
- Client 第三方應(yīng)用程序,比如服務(wù)號開發(fā)商
- Authorization server 授權(quán)服務(wù)器,比如微信官方的服務(wù)器,成功鑒權(quán)之后發(fā)放Access token
- Resource server 資源服務(wù)器,比如頭像,手機,微信朋友關(guān)系等,使用Access token可以訪問受保護資源
- Access token 用于訪問受保護資源的令牌
- Authorization code 用戶授權(quán)Client代表他們訪問受保護資源時生成的中間令牌祟身。Client收到此令牌并將其交換為Access token
- Grant 獲得Access token的方式
- Scope 許可
- 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
如果您授權(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
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)用戶登錄
登錄成功之后引導(dǎo)用戶授權(quán)
用戶同意授權(quán),重定向到第三方的redirect_uri并帶上以下get參數(shù)
- code : Authorization code
- state : 上面?zhèn)鞯膕tate,可驗證是否相同
第二步
第三方發(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 : 訪問令牌過期時可刷新
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 : 訪問令牌過期時可刷新
數(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;
}
}