谷歌API客户端“必须传递刷新令牌或将其作为setAccessToken的一部分设置”

18
我目前遇到了一个非常奇怪的问题,事实上我一直在按照谷歌API文档中的https://developers.google.com/google-apps/calendar/quickstart/php指南操作。我尝试了两次,在第一次尝试时它能够完美地工作,但在访问令牌过期后,由谷歌API文档提供的脚本无法刷新它。
TL;DR
以下是错误消息:
sam@ssh:~$ php www/path/to/app/public/quickstart.php


Fatal error: Uncaught exception 'LogicException' with message 'refresh token must be passed in or set as part of setAccessToken' in /home/pueblo/www/path/to/app/vendor/google/apiclient/src/Google/Client.php:258
Stack trace:
#0 /home/pueblo/www/path/to/app/public/quickstart.php(55): Google_Client->fetchAccessTokenWithRefreshToken(NULL)
#1 /home/pueblo/www/path/to/app/public/quickstart.php(76): getClient()
#2 {main}
  thrown in /home/pueblo/www/path/to/app/vendor/google/apiclient/src/Google/Client.php on line 258

这是我修改过的来自Google的PHP脚本的一部分:
require_once __DIR__ . '/../vendor/autoload.php';

// I don't want the creds to be in my home folder, I prefer them in the app's root
define('APPLICATION_NAME', 'LRS API Calendar');
define('CREDENTIALS_PATH', __DIR__ . '/../.credentials/calendar-php-quickstart.json');
define('CLIENT_SECRET_PATH', __DIR__ . '/../client_secret.json');

我还修改了expandHomeDirectory,这样我就可以在不修改太多代码的情况下“禁用”它:

function expandHomeDirectory($path) {
  $homeDirectory = getenv('HOME');
  if (empty($homeDirectory)) {
    $homeDirectory = getenv('HOMEDRIVE') . getenv('HOMEPATH');
  }
  return $path;
  // return str_replace('~', realpath($homeDirectory), $path);
}

所以为了确认是我错了还是谷歌错了,我做了一个实验:昨晚我通过ssh启动了快速入门脚本来检查它是否工作正常,事实上它是可以工作的。因此我决定今天早上再次检查它是否像在我睡觉之前一样正常工作,但是它并没有。所以我认为谷歌的quickstart.php出了问题。

我希望有人能帮助我,我已经查看了所有其他关于该主题的文章,但它们都过时了。


我认为这个SO问题可以帮助你。 - KENdi
不,事实上它看起来像一个有效的答案,然而这个用户报告的 bug 在他向 Google 提交工单并得到修复后,已经被我正在使用的代码修复了。但还是谢谢你想要帮助我 :) - Samuel Prevost
截至2017年11月20日,Google发布的快速入门PHP示例代码(quickstart.php)仍然存在您所询问的错误。 - gth
9个回答

28

我最近遇到了相同的问题,我用这个方法解决了它。

<?php
 $client->setRedirectUri($this->_redirectURI);
 $client->setAccessType('offline');
 $client->setApprovalPrompt('force');

我解释一下......因为我们没有强制执行approvalPrompt,所以没有返回刷新令牌。离线模式是不够的,我们必须强制执行approvalPrompt。另外,在这两个选项之前必须设置redirectURI。对我来说这很有效。

这是我的完整函数:

<?php
     private function getClient()
     {
        $client = new Google_Client();
        $client->setApplicationName($this->projectName);
        $client->setScopes(SCOPES);
        $client->setAuthConfig($this->jsonKeyFilePath);
        $client->setRedirectUri($this->redirectUri);
        $client->setAccessType('offline');
        $client->setApprovalPrompt('force');

       // Load previously authorized credentials from a file.
       if (file_exists($this->tokenFile)) {
         $accessToken = json_decode(file_get_contents($this->tokenFile), 
         true);
      } else {
        // Request authorization from the user.
        $authUrl = $client->createAuthUrl();
        header('Location: ' . filter_var($authUrl, FILTER_SANITIZE_URL));

        if (isset($_GET['code'])) {
            $authCode = $_GET['code'];
            // Exchange authorization code for an access token.
            $accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
            header('Location: ' . filter_var($this->redirectUri, 
            FILTER_SANITIZE_URL));
            if(!file_exists(dirname($this->tokenFile))) {
                mkdir(dirname($this->tokenFile), 0700, true);
            }

            file_put_contents($this->tokenFile, json_encode($accessToken));
        }else{
            exit('No code found');
        }
    }
    $client->setAccessToken($accessToken);

    // Refresh the token if it's expired.
    if ($client->isAccessTokenExpired()) {

        // save refresh token to some variable
        $refreshTokenSaved = $client->getRefreshToken();

        // update access token
        $client->fetchAccessTokenWithRefreshToken($refreshTokenSaved);

        // pass access token to some variable
        $accessTokenUpdated = $client->getAccessToken();

        // append refresh token
        $accessTokenUpdated['refresh_token'] = $refreshTokenSaved;

        //Set the new acces token
        $accessToken = $refreshTokenSaved;
        $client->setAccessToken($accessToken);

        // save to file
        file_put_contents($this->tokenFile, 
       json_encode($accessTokenUpdated));
    }
    return $client;
}

谢谢你的回答!我希望它能帮助很多人。我已经切换到Apple iCloud的CalDAV API,因为它更简单易用(没有复杂的令牌之类的东西…),但还是谢谢^^ - Samuel Prevost
很高兴为您服务。 - Ulrich Dohou
1
不要忘记每次访问令牌更新时,在保存的文件中设置访问令牌和创建时间。$client->setTokenCallback(...) 提供了一个钩子,可以在客户端(自动)刷新令牌时存储新令牌。如果不这样做,那么您将在最初授权后的一小时内每次 API 调用都会刷新令牌。如果只有一天一次的单个计划 API 调用,则可能还好,但如果运行得更频繁,则会增加很多开销。 - Jason
@UlrichDohou 你写道:“另外,必须在这两个选项之后设置redirectURI”,但是在你的示例中,redirectURI是在这两个选项之前设置的。?? - Andrew
1
虽然你没有在任何地方使用accessTokenUpdated,但它的目的是什么? - Amir Hassan Azimi
显示剩余2条评论

6

我的建议是在获取访问令牌后立即将更新令牌保存为.json文件,如果访问令牌过期,请使用更新令牌。

在我的项目中,工作方式如下:

public static function getClient()
{
    $client = new Google_Client();
    $client->setApplicationName('JhvInformationTable');
    $client->setScopes(Google_Service_Calendar::CALENDAR_READONLY);
    $client->setAuthConfig('credentials.json');
    $client->setAccessType('offline');

    // Load previously authorized credentials from a file.
    $credentialsPath = 'token.json';
    $credentialsPath2 = 'refreshToken.json';
    if (file_exists($credentialsPath)) {
        $accessToken = json_decode(file_get_contents($credentialsPath), true);
    } else {
        // Request authorization from the user.
        $authUrl = $client->createAuthUrl();
        //printf("Open the following link in your browser:\n%s\n", $authUrl);
        //print 'Enter verification code: ';
        $authCode = trim(fgets(STDIN));

        //echo "<script> location.href='".$authUrl."'; </script>";
        //exit;

        $authCode ='********To get code, please uncomment the code above********';

        // Exchange authorization code for an access token.
        $accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
        $refreshToken = $client->getRefreshToken();

        // Check to see if there was an error.
        if (array_key_exists('error', $accessToken)) {
            throw new Exception(join(', ', $accessToken));
        }

        // Store the credentials to disk.
        if (!file_exists(dirname($credentialsPath))) {
            mkdir(dirname($credentialsPath), 0700, true);
        }
        file_put_contents($credentialsPath, json_encode($accessToken));
        file_put_contents($credentialsPath2, json_encode($refreshToken));
        printf("Credentials saved to %s\n", $credentialsPath);
    }
    $client->setAccessToken($accessToken);

    // Refresh the token if it's expired.
    if ($client->isAccessTokenExpired()) {
        $refreshToken = json_decode(file_get_contents($credentialsPath2), true);
        $client->fetchAccessTokenWithRefreshToken($refreshToken);
        file_put_contents($credentialsPath, json_encode($client->getAccessToken()));
    }
    return $client;
}

4
我曾遇到使用新的谷歌API库时出现了同样的问题。搜索解决方案后,我找到了以下链接:RefreshToken Not getting send back after I get new token google sheets API
根据这些信息,我修改了快速入门代码部分以满足我的需求。在与Google进行第一次授权后,我获得了drive-php-quickstart.json,其中包含刷新令牌,有效期为3600秒或一小时。刷新令牌只会发放一次,因此如果丢失,则需要重新进行授权。 因此,为了始终将其保存在drive-php-quickstart.json中,我执行了以下操作:
// Refresh the token if it's expired.
if ($client->isAccessTokenExpired()) {
// save refresh token to some variable
$refreshTokenSaved = $client->getRefreshToken(); 

// update access token
$client->fetchAccessTokenWithRefreshToken($refreshTokenSaved); 

// pass access token to some variable
$accessTokenUpdated = $client->getAccessToken();

// append refresh token
$accessTokenUpdated['refresh_token'] = $refreshTokenSaved;

// save to file
file_put_contents($credentialsPath, json_encode($accessTokenUpdated)); 
}

谢谢你的回答!我会看看它是否是可行的解决方案,因为它将非常有帮助。但我现在非常忙,所以不要期望我回答得太快。 - Samuel Prevost
@SamuelPrevost 我正在使用Sheet API v4,虽然代码相同,但仍然出现“必须传递刷新令牌或将其设置为setAccessToken的一部分”的错误。 - Hamza Zafeer
@HamzaZafeer,你尝试过这个帖子中的所有回复吗?我不知道如何帮助你,已经过去一年了。我甚至不知道什么是“Sheet API v4”。备份你当前的设置,然后尝试这里的每个解决方案,直到找到一个可行的。祝你好运。 - Samuel Prevost

3

对于那些遇到这个问题的人,我想提供一些更新,大多数情况下是因为只有第一个fetchAccessTokenWithAuthCode()命令会生成包含刷新令牌(在技术上永久有效 - 如果您不撤销它,则没有2小时有效期)的凭据。当您获得新凭证时,它会替换原始凭证,但它不包含所需的刷新令牌,因此下次需要更新令牌时,它将崩溃。可以通过将刷新函数替换为例如以下内容轻松解决此问题:

  // Refresh the token if it's expired.
  if ($client->isAccessTokenExpired()) {
    $oldAccessToken=$client->getAccessToken();
    $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
    $accessToken=$client->getAccessToken();
    $accessToken['refresh_token']=$oldAccessToken['refresh_token'];
    file_put_contents($credentialsPath, json_encode($accessToken));
}

现在每次您更新访问令牌时,刷新令牌也会被一起传递。


1
当您将accestoken写入credentialsPath时,需要对其进行序列化。
 // Exchange authorization code for an access token.
    $accessToken = $client->authenticate($authCode);

    // Store the credentials to disk.
    if(!file_exists(dirname($credentialsPath))) {
        mkdir(dirname($credentialsPath), 0700, true);
    }
    $serArray = serialize($accessToken);
    file_put_contents($credentialsPath, $serArray);
    printf("Credentials saved to %s\n", $credentialsPath);

当您从文件中读取时,需要对其进行反序列化。
if (file_exists($credentialsPath)) {
    $unserArray =  file_get_contents($credentialsPath);
    $accessToken = unserialize($unserArray);

}

完整功能
function getClient() {
    $client = new Google_Client();
    // Set to name/location of your client_secrets.json file.
    $client->setAuthConfigFile('client_secret.json');
    // Set to valid redirect URI for your project.
    $client->setRedirectUri('http://localhost');
    $client->setApprovalPrompt('force');

    $client->addScope(Google_Service_YouTube::YOUTUBE_READONLY);
    $client->setAccessType('offline');

    // Load previously authorized credentials from a file.
    $credentialsPath = expandHomeDirectory(CREDENTIALS_PATH);


    if (file_exists($credentialsPath)) {
        $unserArray =  file_get_contents($credentialsPath);
        $accessToken = unserialize($unserArray);

    } else {
        // Request authorization from the user.
        $authUrl = $client->createAuthUrl();
        printf("Open the following link in your browser:\n%s\n", $authUrl);
        print 'Enter verification code: ';
        $authCode = trim(fgets(STDIN));

        // Exchange authorization code for an access token.
        $accessToken = $client->authenticate($authCode);

        // Store the credentials to disk.
        if(!file_exists(dirname($credentialsPath))) {
            mkdir(dirname($credentialsPath), 0700, true);
        }
        $serArray = serialize($accessToken);
        file_put_contents($credentialsPath, $serArray);
        printf("Credentials saved to %s\n", $credentialsPath);
    }

    $client->setAccessToken($accessToken);

    // Refresh the token if it's expired.
    if ($client->isAccessTokenExpired()) {
        $client->refreshToken($client->getRefreshToken());
        file_put_contents($credentialsPath, $client->getAccessToken());
    }
    return $client;
}

1
我遇到了同样的问题,最终解决了:
背景故事:
我也收到了相同的错误信息。下面是我的发现:
这个错误信息:
PHP致命错误:Uncaught LogicException: refresh token must be passed in or set as part of setAccessToken in /Library/WebServer/Documents/Sites/test/scripts/vendor/google/apiclient/src/Google/Client.php:267
指的是更新访问令牌(也称为刷新)方法:
$client->fetchAccessTokenWithRefreshToken($refreshTokenSaved);

为什么会失败?长话短说,当我打印出$accessToken数组时,我意识到它是从解码这个json文件而来的(根据您发布的快速入门代码/谷歌提供的)。

credentials/calendar-php-quickstart.json

我发现错误是由于在print_r时accessToken数组的打印方式造成的: 数组 ( [access_token] => 数组 ( [access_token] => xyz123 [token_type] => Bearer [expires_in] => 3600 [refresh_token] => xsss112222 [created] => 1511379484 )

)

解决方案:

$refreshToken = $accessToken["access_token"]["refresh_token"];

在这行代码之前:

    $client->fetchAccessTokenWithRefreshToken($refreshToken);

我可以在一个小时后的过期时刷新令牌。我认为这篇文章的开发人员假设数组输出如下: Array ( [access_token] => xyz123 [token_type] => Bearer [expires_in] => 3600 [refresh_token] => xsss112222 [created] => 1511379484 )
因此他们认为你可以简单地执行$accessToken["refresh_token"]; 这是不正确的。
现在我们有了$refreshToken的有效值,所以如果你这样做,错误就会消失。我还通过反馈链接向作者更新了这一点,以防其他PHP开发人员遇到这个问题。希望这能帮助到某个人。如果我格式化这篇文章很差,我要道歉,因为我是新手。我只是想分享一下,因为我终于让它工作了。

0
在我的情况下,我忘记将访问类型设置为“离线”,没有这个类型,刷新令牌就无法生成。
$client->setAccessType('offline');

完成这个步骤后,Google文档中提供的示例代码将会正常工作。

// Exchange authorization code for an access token.
// "refresh_token" is returned along with the access token
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
$client->setAccessToken($accessToken);


// Refresh the token if it's expired.
if ($client->isAccessTokenExpired()) {
    $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
    file_put_contents($credentialsPath, json_encode($client->getAccessToken()));
}

0

看了一段时间这段代码之后:

// Exchange authorization code for an access token.
// "refresh_token" is returned along with the access token
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
$client->setAccessToken($accessToken);


// Refresh the token if it's expired.
if ($client->isAccessTokenExpired()) {
    $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
    file_put_contents($credentialsPath, json_encode($client->getAccessToken()));
}

只需要这个更改:

// Exchange authorization code for an access token.
// "refresh_token" is returned along with the access token
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
$client->setAccessToken($accessToken);


// Refresh the token if it's expired.
if ($client->isAccessTokenExpired()) {
    $client->fetchAccessTokenWithRefreshToken($accessToken);
    file_put_contents($credentialsPath, json_encode($client->getAccessToken()));
}

由于此函数 $client->getRefreshToken() 返回 null,如果您直接提供 $accessToken,则可以正常工作并更新您的文件,希望解决某些人的问题。


0
Google已更新其PHP快速入门,并改进了处理此事的方法:
以下是片段:
// Exchange authorization code for an access token.
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
$client->setAccessToken($accessToken);

// Refresh the token if it's expired.
if ($client->isAccessTokenExpired()) {
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
file_put_contents($credentialsPath, json_encode($client->getAccessToken()));
}

1
我使用了最新的快速入门,但仍然出现错误?为什么? - Hamza Zafeer
截至2017年11月20日,Google发布的示例PHP代码quickstart.php仍然存在原帖提到的错误。 - gth

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