经过大量的搜索,
正如谷歌文档所说:
您无需安装任何库即可直接调用 OAuth 2.0 端点。
同样在OpenId:
本文档描述了我们用于身份验证的 OAuth 2.0 实现,它符合 OpenID Connect 规范,并通过了 OpenID 认证。使用 OAuth 2.0 访问 Google API 中的文档也适用于该服务。
因此,我将所有库发送到我的回收站,并使用 REST 和 cURL 来发出我的服务器请求。让我们定义一个类GoogleToken
,其中包含与我们对 Google 服务器的请求相关的属性,例如 url、客户端 ID、客户端密码等:
文件 GoogleToken.php
<?php
namespace test;
/**
* Google authentication Config class
*/
class GoogleToken {
/** See the list: https://developers.google.com/identity/protocols/OAuth2WebServer#creatingclient */
/** the application/client id */
const appId = '';
/** the application/client secret */
const appSecret = '';
/** the application/client redirect uri after login */
const loginUri = 'http://localhost/auth/google/login';
/** the application/client redirect uri after logout */
const logoutUri = 'http://localhost/auth/google/logout';
/** use 'online' to get the access token!
* use 'offline' to get a refresh token along with the access token,
* see also 'prompt'!
*/
const access = 'offline';
/** the application/client approval prompt [auto|force]
* use 'force' to get a refresh token!
*/
const prompt = 'force';
/** the application/client access permissions
* [openid profile email|see https://developers.google.com/identity/protocols/googlescopes]
*/
const scopes = 'openid profile email';
public function __construct() {}
}
上面的类应该被看作是一个编程令牌,它可以帮助其他类实现它们的目标(也许它是一个新的范式,你可以评估它)。实际上,它是一个更复杂的类,可以通过数据 IO 的方法为我们的目的服务,在不接触 php 文件的情况下简化用户管理等。
让我们构建一个向 Google 服务器发出请求并存储(临时)响应的类,即 Google 发送的实际令牌:
文件 GoogleHttpClient.php
这个类的骨架:
public function authenticate($code)
public function verify($access_token)
public function validate($verify_token, $user)
public function refresh($refresh_token)
public function revokeToken($access_token)
public function userInfo($access_token)
public function createAuthUrl()
所有方法都接受参数,因为重定向会丢失内部存储的内容,这就是我们需要存储机制来提供这些参数的原因。Filetest.php
提供了存储类属性并在需要时调用它们的顶层。有趣的部分是该方法authenticate()
应该按顺序进行:
验证请求
用户信息请求
而不是反过来,您将遇到来自 Google 的延迟/错误。班级GoogleHttpClient
:
<?php
namespace test;
/**
* The GoogleHttpClient is a helper class.
*
* As this class is called between different calls, it needs access to a state
* storage mechanism like sessions.
* Even though the methods try to abstract away the complexity of the calls by
* a logical organisation of the actions involved, you are responsible for the
* order or context under which they are called.
*/
class GoogleHttpClient {
/** @var GoogleToken $token The GoogleToken class object */
protected $token;
/**
* @var string[] $auth The authorization data returned by Google in the form:
* Array(
* [access_token] => xxx
* [expires_in] => [unix timestamp]
* [id_token] => xxx.xxx.xxx
* [refresh_token] => xxx
* [token_type] => Bearer
* )
*
* This property should be stored in session (per user) in case we need a
* verification or refresh.
*/
protected $auth;
/**
* @var string[] $verify The response data returned by Google in the form:
* Array(
* [issued_to] => xxx.apps.googleusercontent.com (@see GoogleToken['values']['appId'])
* [audience] => xxx.apps.googleusercontent.com (@see GoogleToken['values']['appId'])
* [user_id] => xxx
* [scope] => xxx (@see createAuthUrl())
* [expires_in] => xxx (<= 3600)
* [email] => xxx
* [verified_email] => 1
* [access_type] => xxx (@see createAuthUrl())
* )
*
* You shouldn't store this property in session.
*/
protected $verify;
/**
* @var string[] $user The user data returned by Google.
*
* You should verify this property with your native authorization system and
* integrate with it finally.
*/
protected $user;
/** @var string[] $error The error returned by Google */
protected $error;
/**
* @var boolean $log If you should log errors and assigned properties for debuging
*/
protected $log;
const INVALID_TOKEN = 1;
/**
* Constructor.
*
* preconditions: a GoogleToken should be passed.
*
* postconditions:
*
* @param GoogleToken $token The GoogleToken that contains initialization values
* @param boolean $log True, if we want to log errors and assigned properties
*
* @return GoogleHttpClient A GoogleHttpClient object
*/
public function __construct(GoogleToken $token, $log = false) {
$this->token = $token;
$this->log = $log;
}
/**
* It makes a authentication request to Google.
*
* preconditions: after the call to Google at this link, @see createAuthUrl(),
* Google responds by making a call back with a unique id that should be
* passed as argument.
*
* postconditions: it sets properties $auth, $user, $verify and $error.
* On error, $auth, $verify and $error contain the responses returned by
* Google depending at the method where error appeared, @see verify(), @see validate().
* If the authentication succeeds then, $error is null.
*
* @param string $code A unique request id issued by Google
*
* @return boolean True on success
*/
public function authenticate($code) {
$client_id = $this->token::appId;
$client_secret = $this->token::appSecret;
$redirect_uri = $this->token::loginUri;
$url = 'https://accounts.google.com/o/oauth2/token';
$curlPost = 'client_id=' . $client_id . '&client_secret=' . $client_secret . '&redirect_uri=' . $redirect_uri . '&code='. $code . '&grant_type=authorization_code';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $curlPost);
$data = json_decode(curl_exec($ch), true);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http_code != 200) {
if ($this->log)
error_log(__CLASS__ . '::authenticate() error: http code not 200. Responded: '.print_r($data, true));
$this->error = $data;
} else {
if ($this->log)
error_log(__CLASS__ . '::auth='.print_r($data, true));
$this->verify($data['access_token']);
$this->user = $this->userInfo($data['access_token']);
$this->validate($this->verify, $this->user);
}
$this->auth = $data;
if ($this->error)
return false;
else
return true;
}
/**
* It requests for a verification token from Google.
*
* preconditions: it is called from @see authenticate().
* Calling this method from redirects means all properties are null and we
* pass session data.
*
* postconditions: it sets property $verify.
* On error, $verify and $error contain the responses returned by Google.
* If the request succeeds then, $error is null.
*
* @param string $access_token Google's authorization response under key 'access_token'
*
* @return boolean True on success
*/
public function verify($access_token) {
$client_id = $this->token::appId;
$client_secret = $this->token::appSecret;
$url = 'https://www.googleapis.com/oauth2/v2/tokeninfo';
$curlPost = 'access_token='. $access_token;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $curlPost);
$data = json_decode(curl_exec($ch), true);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http_code != 200) {
if ($this->log)
error_log(__CLASS__ . '::verify() error: http code not 200. Responded: '.print_r($data, true));
$this->error = $data;
} else {
if ($this->log)
error_log(__CLASS__ . '::verify='.print_r($data, true));
}
$this->verify = $data;
if ($this->error)
return false;
else
return true;
}
/**
* It compares properties $verify with $user and GoogleToken.
*
* preconditions: it is called from @see authenticate().
* Calling this method from redirects means all properties are null and we
* pass session data.
*
* postconditions: on failed validation, $error contains a custom error with fields:
* -error_id: a number
* -error_description: a message.
* If the verification succeeds then, $error is null.
*
* @param string[] $verify_token Google's verification response token
* @param string $user Google's user info response
*
* @return boolean True on success
*/
public function validate($verify_token, $user) {
if (($verify_token['user_id'] != $user['id']) || ($verify_token['email'] != $user['email']) || ($verify_token['issued_to'] != $this->token::appId)) {
$this->error = array('error_id' => self::INVALID_TOKEN, 'error_description' => 'Access token does not pass validation!');
if ($this->log)
error_log(__CLASS__ . '::error='.print_r($this->error, true));
}
if ($this->error)
return false;
else
return true;
}
/**
* It is called when property $auth has expired.
*
* preconditions: authentication has been run, @see authenticate().
* Calling this method from redirects means all properties are null and we
* pass session data.
*
* postconditions: it re-sets property $auth.
* On error, $auth and $error contain the responses returned by Google.
* If the refresh succeeds then, $error is null.
*
* @param string $refresh_token Google's authorization response under key 'refresh_token'
*
* @return boolean True on success
*/
public function refresh($refresh_token) {
$client_id = $this->token::appId;
$client_secret = $this->token::appSecret;
$url = 'https://accounts.google.com/o/oauth2/token';
$curlPost = 'client_id=' . $client_id . '&client_secret=' . $client_secret . '&refresh_token='. $refresh_token . '&grant_type=refresh_token';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $curlPost);
$data = json_decode(curl_exec($ch), true);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http_code != 200) {
if ($this->log)
error_log(__CLASS__ . '::refresh() error: http code not 200. Responded: '.print_r($data, true));
$this->error = $data;
} else {
if ($this->log)
error_log(__CLASS__ . '::auth='.print_r($data, true));
}
$this->auth = $data;
if ($this->error)
return false;
else
return true;
}
/**
* It un-registers application from user's account but id does not (and cannot)
* logout user from Google.
*
* preconditions: authentication has been run, @see authenticate().
* Calling this method from redirects means all properties are null and we
* pass session data.
*
* postconditions: it un-registers application from user's account.
* On error, $error contains the response returned by Google.
* If the revoke succeeds then, all properties are nullified.
*
* @param string $access_token Google's authorization response under key 'access_token'
*
* @return boolean True on success
*/
public function revokeToken($access_token) {
$client_id = $this->token::appId;
$client_secret = $this->token::appSecret;
$url = 'https://accounts.google.com/o/oauth2/revoke';
$curlPost = 'token=' . $access_token;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $curlPost);
$data = json_decode(curl_exec($ch), true);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http_code != 200) {
if ($this->log)
error_log(__CLASS__ . '::revokeToken() error: http code not 200. Responded: '.print_r($data, true));
$this->error = $data;
} else {
$this->auth = null;
$this->user = null;
$this->verify = null;
$this->error = null;
if ($this->log)
error_log(__CLASS__ . '::revokeToken() run and erased all properties!');
}
if ($this->error)
return false;
else
return true;
}
/**
* It is called from @see authenticate() or you can call it independently at
* a later time.
*
* preconditions: authentication has been run, @see authenticate().
* Calling this method from redirects means all properties are null and we
* pass session data.
*
* postconditions: it sets property $user.
* On error, $user and $error contain the responses returned by Google.
* If the request succeeds then, $error is null.
*
* @param string $access_token Google's authorization response under key 'access_token'
*
* @return string[] A hash array of user's data
*/
public function userInfo($access_token) {
/** format of data:
* Array(
* [id] => xxx
* [email] => xxx@gmail.com
* [verified_email] => 1
* [name] => xxx xxx
* [given_name] => xxx
* [family_name] => xxx
* [picture] => https://lh6.googleusercontent.com/.../photo.jpg?sz=50
* [locale] => en
* )
*/
$url = 'https://www.googleapis.com/userinfo/v2/me';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer '. $access_token));
$data = json_decode(curl_exec($ch), true);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http_code != 200) {
if ($this->log)
error_log(__CLASS__ . '::userInfo() error: http code not 200. Responded: '.print_r($data, true));
$this->error = $data;
} else {
if ($this->log)
error_log(__CLASS__ . '::user='.print_r($data, true));
}
$this->user = $data;
return $this->user;
}
/**
* Creates a authentication link to Google servers.
*/
public function createAuthUrl() {
$scopes = \urlencode($this->token::scopes);
$redirect = \urlencode($this->token::loginUri);
$appId = $this->token::appId;
$access = $this->token::access;
$prompt = $this->token::prompt;
return "https://accounts.google.com/o/oauth2/auth?scope={$scopes}&redirect_uri={$redirect}&response_type=code&client_id={$appId}&access_type={$access}&approval_prompt={$prompt}";
}
/**
* Returns property $token
*/
public function getToken() {
return $this->token;
}
/**
* Returns property $auth
*/
public function getAuthToken() {
return $this->auth;
}
/**
* Returns property $verify
*/
public function getVerifyToken() {
return $this->verify;
}
/**
* Returns property $user
*/
public function getUser() {
return $this->user;
}
/**
* Returns property $error
*/
public function getError() {
return $this->error;
}
}
现在我们可以调用谷歌并制作一个登录应用程序:
文件 test.php
<?php
namespace test;
require_once __DIR__ . '/GoogleToken.php';
require_once __DIR__ . '/GoogleHttpClient.php';
if (\session_status() != PHP_SESSION_ACTIVE) {
session_start();
}
$whoami = $_SERVER['REQUEST_URI'];
$login = $logout = false;
if (\strpos($whoami, '/login') !== false)
$login = true;
if (\strpos($whoami, '/logout') !== false)
$logout = true;
$token = new GoogleToken();
$client = new GoogleHttpClient($token, true); // enable log
$loginUri = GoogleToken::loginUri;
$logoutUri = GoogleToken::logoutUri;
/* emulates...
* LOGIN endpoint
* --------------
*/
if ($login) {
if (isset($_GET['code'])) {
if ($client->authenticate($_GET['code']) {
$_SESSION['google']['auth'] = $auth;
$_SESSION['google']['auth']['expires_in'] = \date("Y-m-d H:i:s", \time() + $_SESSION['google']['auth']['expires_in']);
$_SESSION['google']['user'] = $user;
}
}
header('Location: ' . filter_var('http://localhost', FILTER_SANITIZE_URL));
}
/* emulates...
* LOGOUT endpoint
* ---------------
*/
if ($logout) {
$client->revokeToken($_SESSION['google']['auth']['access_token']);
unset($_SESSION['google']);
header('Location: ' . filter_var('http://localhost', FILTER_SANITIZE_URL));
}
$userData = '';
/* emulates...
* Redirect from:
* --------------
* - LOGIN endpoint
*/
if (isset($_SESSION['google'])) {
if(\time() > \strtotime($_SESSION['google']['auth']['expires_in'])) {
if(!$client->refresh($_SESSION['google']['auth']['access_token'])) {
$client->revokeToken();
unset($_SESSION['google']);
header('Location: ' . filter_var('http://localhost', FILTER_SANITIZE_URL));
}
}
$userData = $_SESSION['google']['user'];
/* emulates...
* INITIAL endpoint
* ----------------
* or,
* Redirect from:
* --------------
* - LOGOUT endpoint or,
* - failed REFRESH
*/
} else {
$authUrl = $client->createAuthUrl();
}
$out0 = <<<EOT
<html>
<head>
<title>Google REST OAuth v.2.0 Login test</title>
</head>
<body>
EOT;
if (isset($authUrl)) {
$d = \urldecode($authUrl);
$out0 .= <<<EOT
<p>decoded authUrl = '{$d}'</p>
EOT;
}
$out0 .= <<<EOT
<h2>PHP Google OAuth 2.0 Login</h2>
EOT;
if (isset($authUrl)) {
$out0 .= <<<EOT
<p><a href="{$authUrl}">Login with Google API</a></p>
EOT;
} else {
$out0 .= <<<EOT
<p>Welcome <img src="{$userData['picture']}" style="width:50px;height:50px;"></img> {$userData['name']}.</p>
<p>Your email: {$userData['email']}</p>
<p><a href={$logoutUri}>Logout</a></p>
EOT;
}
$e = \nl2br(\htmlspecialchars(\print_r($_SESSION, true)));
$out0 .= <<<EOT
<p><h3>SESSION:</h3></p>
<p>{$e}</p>
</body>
</html>
EOT;
echo $out0;
正如我们所看到的,类GoogleHttpClient
完成了所有工作,但test.php
实际上文件应该被另一个处理我们的端点/请求的层替换GoogleHttpClient
。
玩得开心!