如何在 PHP 网站中正确实现 Google OAuth2 登录与令牌管理

本文详解 google oauth2 在 php 中的正确实践:强调访问令牌(access token)按用户生成且短期有效,应仅存储刷新令牌(refresh token)于数据库,访问令牌则安全保存于加密 session 中;并指出错误登出方式的危害及修正方案。

Google OAuth2 的核心设计原则是以用户为中心的身份认证:每个已授权用户都会获得独立的、有时效性的访问令牌(Access Token),默认有效期为 1 小时(3600 秒),而刷新令牌(Refresh Token)则是长期有效的凭证(除非被显式撤销或用户主动解绑),用于在访问令牌过期后静默获取新令牌。

你遇到的 401 Unauthorized 错误,根本原因在于当前逻辑混淆了两类令牌的职责与生命周期:

  • 错误做法:将 access_token 直接存入数据库,并在每次请求时无条件复用;登出时调用 $client->revokeToken($accessToken) —— 这不仅使该 token 失效,更可能影响用户在其他已授权应用中的登录状态(因 Google 的 revoke 操作是全局的);
  • 正确做法仅将 refresh_token 安全持久化(如数据库),并将 access_token 存于加密的 PHP Session 中。每次请求前检查 Session 中的 access token 是否有效;若已过期或不存在,则用数据库中存储的 refresh token 向 Google 请求新 access token,并更新 Session。

以下是重构后的关键逻辑示例(基于 google/apiclient:^2.0):

setAuthConfig($_SERVER['DOCUMENT_ROOT'] . '/login/credentials.json');
$client->setAccessType('offline');   // 必须启用,才能获得 refresh_token
$client->setPrompt('consent');        // 强制重新授权,确保获取新 refresh_token(开发调试用)
$client->addScope(['email', 'profile']);

$dbx = new db();

// 1. 尝试从 Session 获取有效的 access token
$accessToken = $_SESSION['google_access_token'] ?? null;
if ($accessToken && !isset($accessToken['expires_in']) || time() >= ($accessToken['created_at'] + $accessToken['expires_in'])) {
    // Token 已过期或格式异常 → 清除并准备刷新
    unset($_SESSION['google_access_token']);
    $accessToken = null;
}

// 2. 若 Session 中无有效 token,尝试用 refresh_token 刷新
if (!$accessToken) {
    $refreshToken = $dbx->get_refresh_token(); // 仅读取 refresh_token!
    if ($refreshToken) {
        $client->fetchAccessTokenWithRefreshToken($refreshToken);
        $newToken = $client->getAccessToken();
        if (isset($newToken['access_token'])) {
            $newToken['created_at'] = time();
            $_SESSION['google_access_token'] = $newToken;
            $accessToken = $newToken;
        }
    }
}

// 3. 若仍无有效 token,且有授权码(首次登录回调)
if (!$accessToken && isset($_GET['code'])) {
    $tokenResponse = $client->fetchAccessTokenWithAuthCode($_GET['code']);
    if (isset($tokenResponse['access_token'])) {
        // ✅ 仅在此处持久化 refresh_token(一次即可)
        if (isset($tokenResponse['refresh_token'])) {
            $dbx->set_refresh_token($tokenResponse['refresh_token']);
        }
        $tokenResponse['created_at'] = time();
        $_SESSION['google_access_token'] = $tokenResponse;
        $accessToken = $tokenResponse;
    }
}

// 4. 最终校验:若仍未获得有效 token,重定向至授权页
if (!$accessToken) {
    header('Location: ' . $client->createAuthUrl());
    exit;
}

// 设置 client 的 access token 并调用 API
$client->setAccessToken($accessToken);
$service = new Google_Service_Oauth2($client);
$userdata = $service->userinfo->get();
?>

登出逻辑应彻底重构(关键修正)

立即学习“PHP免费学习笔记(深入)”;

delete_refresh_token(); // 删除数据库中该用户的 refresh_token

// 可选:重定向到首页或登录页
header('Location: /login.php');
exit;
?>

重要注意事项总结

  • ? 安全性:refresh_token 是长期凭证,必须严格保护(数据库加密存储、最小权限访问);access_token 绝不落盘,仅驻留于加密 Session;
  • ? Scope 与 Consent:setPrompt('consent') 仅在开发/测试阶段使用,上线后应移除或改用 'select_account',避免频繁强制授权;
  • ? 错误处理:生产环境需捕获 Google_Exception,特别是 fetchAccessTokenWithRefreshToken() 失败时(如 refresh_token 被用户在 Google 账户中手动撤销),此时应清空数据库记录并引导用户重新登录;
  • ? 多用户支持:db.class.php 中的 get_refresh_token() 和 set_refresh_token() 方法必须绑定当前用户标识(如通过 $_SESSION['user_id'] 或 OAuth id_token 解析的 sub 字段),确保令牌隔离。

遵循以上模式,你将构建出符合 OAuth2 最佳实践、安全可靠且可扩展的 Google 登录系统。