如何使用Google API客户端刷新令牌?

99
我一直在尝试使用Google Analytics API(V3)并遇到了一些错误。首先,所有设置都是正确的,并且在我的测试帐户中工作正常。但是当我想要从另一个配置文件ID(相同的Google账户/ GA账户)获取数据时,我会收到403错误。奇怪的是,来自某些GA账户的数据将返回数据,而其他账户会产生此错误。
我已经撤销了令牌并重新进行了身份验证,现在似乎我可以从所有帐户中获取数据。问题解决了吗?不是。由于访问密钥将过期,我将再次遇到相同的问题。
如果我理解正确,可以使用refreshToken获取新的authenticationTooken。
问题是,当我运行:
$client->refreshToken(refresh_token_key) 

返回以下错误:

Error refreshing the OAuth2 token, message: '{ "error" : "invalid_grant" }'

我已经检查了refreshToken方法的代码,并跟踪请求到“apiOAuth2.php”文件。所有参数都被正确发送。在该方法中,grant_type硬编码为'refresh_token',所以很难理解出错的原因。参数数组如下:

Array ( [client_id] => *******-uqgau8uo1l96bd09eurdub26c9ftr2io.apps.googleusercontent.com [client_secret] => ******** [refresh_token] => 1\/lov250YQTMCC9LRQbE6yMv-FiX_Offo79UXimV8kvwY [grant_type] => refresh_token )

具体步骤如下。

$client = new apiClient();
$client->setClientId($config['oauth2_client_id']);
$client->setClientSecret($config['oauth2_client_secret']);
$client->setRedirectUri($config['oauth2_redirect_uri']);
$client->setScopes('https://www.googleapis.com/auth/analytics.readonly');
$client->setState('offline');

$client->setAccessToken($config['token']); // The access JSON object.

$client->refreshToken($config['refreshToken']); // Will return error here

这是一个错误还是我完全误解了什么?


不知道是不是一个 bug 或者其他问题,但我目前正在使用原始的 CURL http 请求刷新访问令牌,它正常工作。 - gremo
Seorch... 你解决了这个问题吗?我也遇到了同样的问题。 - Brian Vanderbusch
@gremo,你能分享一下你在这里使用的原始CURL HTTP请求吗?这将非常有帮助。谢谢! - Silver Ringvee
18个回答

80

所以我终于弄清楚了如何做到这一点。基本思想是,你有第一次请求身份验证时获取的令牌。这个第一个令牌有一个刷新令牌。第一个原始令牌在一个小时后过期。一个小时后,您必须使用第一个令牌的刷新令牌来获取一个新的可用令牌。您可以使用 $client->refreshToken($refreshToken) 来检索新令牌。我将称之为“临时令牌”。您还需要存储此临时令牌,因为它在一个小时后也会过期,并且请注意,它没有与其关联的刷新令牌。为了获得新的临时令牌,您需要使用之前使用的方法并使用第一个令牌的刷新令牌。我附上了下面的代码,虽然有些丑陋,但我是新手...

//pull token from database
$tokenquery="SELECT * FROM token WHERE type='original'";
$tokenresult = mysqli_query($cxn,$tokenquery);
if($tokenresult!=0)
{
    $tokenrow=mysqli_fetch_array($tokenresult);
    extract($tokenrow);
}
$time_created = json_decode($token)->created;
$t=time();
$timediff=$t-$time_created;
echo $timediff."<br>";
$refreshToken= json_decode($token)->refresh_token;


//start google client note:
$client = new Google_Client();
$client->setApplicationName('');
$client->setScopes(array());
$client->setClientId('');
$client->setClientSecret('');
$client->setRedirectUri('');
$client->setAccessType('offline');
$client->setDeveloperKey('');

//resets token if expired
if(($timediff>3600)&&($token!=''))
{
    echo $refreshToken."</br>";
    $refreshquery="SELECT * FROM token WHERE type='refresh'";
    $refreshresult = mysqli_query($cxn,$refreshquery);
    //if a refresh token is in there...
    if($refreshresult!=0)
    {
        $refreshrow=mysqli_fetch_array($refreshresult);
        extract($refreshrow);
        $refresh_created = json_decode($token)->created;
        $refreshtimediff=$t-$refresh_created;
        echo "Refresh Time Diff: ".$refreshtimediff."</br>";
        //if refresh token is expired
        if($refreshtimediff>3600)
        {
            $client->refreshToken($refreshToken);
        $newtoken=$client->getAccessToken();
        echo $newtoken."</br>";
        $tokenupdate="UPDATE token SET token='$newtoken' WHERE type='refresh'";
        mysqli_query($cxn,$tokenupdate);
        $token=$newtoken;
        echo "refreshed again";
        }
        //if the refresh token hasn't expired, set token as the refresh token
        else
        {
        $client->setAccessToken($token);
           echo "use refreshed token but not time yet";
        }
    }
    //if a refresh token isn't in there...
    else
    {
        $client->refreshToken($refreshToken);
        $newtoken=$client->getAccessToken();
        echo $newtoken."</br>";
        $tokenupdate="INSERT INTO token (type,token) VALUES ('refresh','$newtoken')";
        mysqli_query($cxn,$tokenupdate);
        $token=$newtoken;
        echo "refreshed for first time";
    }      
}

//if token is still good.
if(($timediff<3600)&&($token!=''))
{
    $client->setAccessToken($token);
}

$service = new Google_DfareportingService($client);

58
应该使用 $client->isAccessTokenExpired(),而不是检查3600秒。 - Gaurav Gupta
3
小更新。在最新版本中,当你请求刷新令牌时,返回的新访问令牌现在会带有一个新的刷新令牌。因此,实质上,你可以使用更新后的 JSON 令牌替换前面的 JSON 令牌,无需再保留最初的访问令牌。 - skidadon
1
请注意,$client->isAccessTokenExpired()仍然只会检查本地保存的时间以确定令牌是否过期。令牌可能仍然已经过期,只有在尝试使用时本地应用程序才会真正知道。在这种情况下,API客户端将返回异常,并且不会自动刷新令牌。 - Jason
@Jason,我认为现在不是这样的。我在“isAccessTokenExpired”方法中看到以下返回语句:return ($created + ($this->token['expires_in'] - 30)) < time(); - sudip

48
问题在于刷新令牌:
[refresh_token] => 1\/lov250YQTMCC9LRQbE6yMv-FiX_Offo79UXimV8kvwY
当带有'/'的字符串被json编码时,它会被转义成'\',因此您需要将其删除。
在您的情况下,刷新令牌应为:
1/lov250YQTMCC9LRQbE6yMv-FiX_Offo79UXimV8kvwY

我假设你已经打印了谷歌返回的json字符串,并将令牌复制并粘贴到代码中,因为如果你 json_decode 它,它将正确地为您删除'\'


1
令人惊叹的提及,让我的一天都变得美好!节省了好几个小时! - Mircea Sandu
你救了我的一天! - Truong Dang
我希望我能给这个点赞100次。在尝试了一切让令牌工作之后,盯着“bad grant”消息数小时后,我差点用键盘打出一个洞。该死的谷歌啊,为什么要使用斜杠?就为什么? - Askerman

21

以下是设置令牌的片段,但在此之前请确保访问类型已设置为离线

if (isset($_GET['code'])) {
  $client->authenticate();
  $_SESSION['access_token'] = $client->getAccessToken();
}

刷新令牌

$google_token= json_decode($_SESSION['access_token']);
$client->refreshToken($google_token->refresh_token);

这将刷新您的令牌,您需要在会话中更新它,您可以执行以下操作。
 $_SESSION['access_token']= $client->getAccessToken()

1
你让我感到非常开心 :) 非常感谢,比我想象中简单得多,因为我花了很多时间却一无所获 :D - T.B Ygg

16

访问类型应设置为offlinestate是你自己使用的变量,而不是API使用的变量。

确保您拥有客户端库的最新版本并添加:

$client->setAccessType('offline');

请查看形成URL以了解参数的说明。


谢谢 jk。我已经下载了最新版本,并撤销了我的账户对该应用程序的访问权限。然后我再次授权并存储了accessToken和refreshToken。问题是,即使setAccessType被省略,我始终会收到一个refreshToken。无论如何,当我运行$client->refreshToken(refresh-token-key)时,仍然会收到“invalid_grant”错误。我已经检查了auth-url,并将其默认设置为“force”。如果我将其更改为“auto”并运行authenticate方法,由于已经授予访问权限,因此不会被重定向。但是响应是一个没有刷新的accessToken。有什么想法吗? - seorch.me
@seorch.me 听起来很疯狂,但是你是否需要设置一个新的 $client ($client = new apiClient();) 来使用刷新令牌? - jk.
1
@seorch.me 在授权期间获取新的刷新令牌,您必须设置 $client->setApprovalPrompt('force')$client->setAccessType('offline')。如果不强制用户批准访问范围,Google 将假定您将继续使用旧的刷新令牌。 - Jason

14

@uri-weg发布的答案对我有效,但我觉得他的解释不太清楚,让我稍微改一下。

在第一次访问权限序列期间,在回调中,当您到达接收身份验证代码的点时,您必须保存访问令牌和刷新令牌

原因是Google API仅在提示访问权限时向您发送带有刷新令牌的访问令牌。下一个访问令牌将不会发送任何刷新令牌(除非您使用approval_prompt=force选项)。

您第一次收到的刷新令牌将保持有效,直到用户撤销访问权限。

在简单的PHP中,回调序列的示例可能如下所示:

// init client
// ...

$authCode = $_GET['code'];
$accessToken = $client->authenticate($authCode);
// $accessToken needs to be serialized as json
$this->saveAccessToken(json_encode($accessToken));
$this->saveRefreshToken($accessToken['refresh_token']);

然后,在简单的php中,连接序列将是:

// init client
// ...

$accessToken = $this->loadAccessToken();
// setAccessToken() expects json
$client->setAccessToken($accessToken);

if ($client->isAccessTokenExpired()) {
    // reuse the same refresh token
    $client->refreshToken($this->loadRefreshToken());
    // save the new access token (which comes without any refresh token)
    $this->saveAccessToken($client->getAccessToken());
}

完美,工作很多。唯一需要说的是,您应该解释需要传递JSON对象而不仅仅是令牌作为字符串。 - Oliver Bayes-Shelton
@OliverBayes-Shelton 你好。谢谢。我以为 // setAccessToken() expects json 就足够了。还是说它是针对代码的另一部分? - Daishi
这对我非常有效,但您知道这段代码是否处理由于超过50个令牌刷新而导致令牌过期的情况吗?有关“令牌过期”的详细信息,请访问此处:https://developers.google.com/identity/protocols/OAuth2#expiration - Bjorn
看起来最新的2.0版本现在会在访问令牌数组中返回刷新令牌。这意味着保存访问令牌也会保存刷新令牌,因为刷新令牌已经包含在内了。针对刷新令牌过期的响应,我猜需要进行显式测试和处理 - 记住50个限制是“每个用户每个客户端”,即每个客户端50个,所以你不太可能达到它,特别是如果你使用包括范围来组合令牌。 - Brian C

10

这是我在项目中正在使用的代码,它运行良好:

public function getClient(){
    $client = new Google_Client();
    $client->setApplicationName(APPNAME);       // app name
    $client->setClientId(CLIENTID);             // client id
    $client->setClientSecret(CLIENTSECRET);     // client secret 
    $client->setRedirectUri(REDIRECT_URI);      // redirect uri
    $client->setApprovalPrompt('auto');

    $client->setAccessType('offline');         // generates refresh token

    $token = $_COOKIE['ACCESSTOKEN'];          // fetch from cookie

    // if token is present in cookie
    if($token){
        // use the same token
        $client->setAccessToken($token);
    }

    // this line gets the new token if the cookie token was not present
    // otherwise, the same cookie token
    $token = $client->getAccessToken();

    if($client->isAccessTokenExpired()){  // if token expired
        $refreshToken = json_decode($token)->refresh_token;

        // refresh the token
        $client->refreshToken($refreshToken);
    }

    return $client;
}

6

那个答案帮了我很多,伙计。你可能为我节省了很多时间。太感谢了! 我在我的Debian机器上执行了sudo apt-get install ntp来安装NTP。它同步了时钟,问题得到解决。 - Szymon Sadło

5
有时候使用$client->setAccessType ("offline");并不能生成刷新令牌。
可以试试这个方法:
$client->setAccessType ("offline");
$client->setApprovalPrompt ("force"); 

更具体地说,看起来刷新令牌包含在您的第一个授权中。如果您保存并使用它,我相信(根据其他人,未经验证)刷新令牌将继续返回。文档还现在表示,如果他们有刷新令牌,他们将自动刷新访问令牌,这意味着只是安全地管理刷新令牌的问题。setApprovalPrompt('force')确实会强制发出刷新令牌;如果没有它,您将不会得到另一个。 - Brian C

4

注意:如果您拥有刷新令牌,则 3.0 版的Google Analytics API 在访问令牌过期时会自动刷新,因此您的脚本无需使用refreshToken

(请参阅 auth/apiOAuth2.php 中的Sign函数。)


“自动刷新”是指我只需请求getAccessToken(),就会得到一个已刷新的令牌吗?但是我必须先从数据库中设置刷新令牌,对吧?否则,刷新将在没有刷新令牌的情况下进行,我认为这不会起作用。 - ninsky

3

在初始授权请求期间,您需要将访问令牌保存到文件或数据库中作为JSON字符串,并将访问类型设置为离线 $client->setAccessType("offline")

然后,在后续的API请求中,从您的文件或数据库中获取访问令牌并将其传递给客户端:

$accessToken = json_decode($row['token'], true);
$client->setAccessToken($accessToken);

现在您需要检查令牌是否过期:
if ($client->isAccessTokenExpired()) {
    // access token has expired, use the refresh token to obtain a new one
    $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
    // save the new token to file or db
    // ...json_encode($client->getAccessToken())
fetchAccessTokenWithRefreshToken()函数将为您执行操作并提供新的访问令牌,保存回您的文件或数据库。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接