Google OAuth刷新令牌能否跨多域名使用?令牌失效问题排查
问题分析与解决方案
核心问题
三个站点共用同一credentials.json和刷新令牌(Refresh Token),且均已将站点域名加入OAuth重定向URL列表,但所有站点的访问令牌(Access Token)过期后,调用fetchAccessTokenWithRefreshToken时均触发Invalid Grant - Bad request错误。手动重新授权生成的新令牌可正常使用约1个月,但到期后仍无法自动刷新。
可能原因
1. 跨站点复用刷新令牌的安全限制
Google OAuth的刷新令牌与**生成该令牌时的授权请求上下文(尤其是重定向URL)**绑定。即使所有站点的重定向URL都在允许列表中,若三个站点复用的是某一个站点授权生成的刷新令牌,其他站点在使用该令牌刷新时,Google的安全校验会识别到请求来源的上下文不一致,从而拒绝刷新请求,返回invalid_grant。
2. 刷新令牌自动失效的常见场景
Google的刷新令牌本身没有默认1个月过期的规则,但以下情况会导致令牌失效:
- 用户在Google账号的「安全中心」主动撤销了对该应用的授权
- OAuth凭证的Client Secret被重置过
- 用户的Google账号密码发生变更
- 同一用户对该应用的有效刷新令牌数量超过50个(Google默认限制,超出后最早生成的令牌会被自动作废)
- 应用请求的权限范围(Scopes)发生变更,但未重新获取用户授权
3. 代码层面的潜在问题
- 检查
tokens.json中存储的刷新令牌是否完整,是否在跨站点复制过程中出现字符丢失或篡改 - 当前代码未对
fetchAccessTokenWithRefreshToken的返回结果做错误判断,若刷新失败,仍会尝试将无效的令牌写入文件,导致后续请求重复报错 - 确认授权流程中
setAccessType('offline')和setPrompt('select_account consent')参数已正确设置,确保能获取到长期有效的刷新令牌
解决方案
- 为每个站点单独生成刷新令牌:每个站点各自执行一次完整的用户授权流程,生成对应自身重定向URL的刷新令牌,避免跨站点复用同一令牌。
- 添加刷新错误处理逻辑:在代码中对
fetchAccessTokenWithRefreshToken的返回结果进行校验,若返回错误(如invalid_grant),则触发重新授权流程,示例修改如下:
if ($client->getRefreshToken()) { $newToken = $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken()); if (array_key_exists('error', $newToken)) { // 刷新失败,触发重新授权 $authUrl = $client->createAuthUrl(); printf("Open the following link in your browser:\n%s\n", $authUrl); print 'Enter verification code: '; $authCode = trim(fgets(STDIN)); $accessToken = $client->fetchAccessTokenWithAuthCode($authCode); $client->setAccessToken($accessToken); if (array_key_exists('error', $accessToken)) { throw new Exception(join(', ', $accessToken)); } } else { $client->setAccessToken($newToken); } }
- 检查OAuth凭证配置:登录Google Cloud Console,确认所有站点的重定向URL已正确添加,OAuth凭证状态正常,Client Secret未被重置。
- 验证刷新令牌有效性:可通过Google OAuth的令牌信息接口检查刷新令牌是否有效(PHP客户端可调用对应方法完成验证)。
附用户提供的原代码
public function getClient(){ $client = new \Google_Client(); $client->setApplicationName('Gmail API PHP Quickstart'); $client->setScopes(\Google_Service_Gmail::GMAIL_MODIFY); $client->setAuthConfig(APPPATH.'Config/Google/credentials.json'); $client->setAccessType('offline'); $client->setPrompt('select_account consent'); $tokenPath = APPPATH.'Config/Google/tokens.json'; $token_json = file_get_contents($tokenPath); if (!empty($token_json)) { $accessToken = json_decode($token_json, true); $client->setAccessToken($accessToken); } // If there is no previous token or it's expired. if ($client->isAccessTokenExpired()) { // Refresh the token if possible, else fetch a new one. if ($client->getRefreshToken()) { $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken()); } 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->fetchAccessTokenWithAuthCode($authCode); $client->setAccessToken($accessToken); // Check to see if there was an error. if (array_key_exists('error', $accessToken)) { throw new Exception(join(', ', $accessToken)); } } // Save the token to a file. if (!file_exists(dirname($tokenPath))) { mkdir(dirname($tokenPath), 0700, true); } $ok = file_put_contents($tokenPath, json_encode($client->getAccessToken())); } return $client; }
内容的提问来源于stack exchange,提问作者Antony
相关产品推荐
相关产品推荐

