PHPMailer使用Gmail XOAUTH2发信如何自动获取新refresh_token
问题核心结论
首先明确两点:
- 你遇到的refresh_token频繁过期问题,本质是Google OAuth应用配置错误导致的,正常配置下的refresh_token有效期接近永久(仅当账号密码修改、手动撤销授权、连续6个月未使用时才会失效),不需要频繁更新。
- Google OAuth2授权流程必须经过账号持有人的显式交互确认,不存在纯PHP后端无人工操作自动获取新refresh_token的方案,这是OAuth协议的安全机制决定的,无法绕过。
第一步:修复OAuth配置,拿到长期有效refresh_token
按以下检查调整配置,从根源解决token频繁过期问题:
- 进入Google Cloud控制台对应项目的「OAuth同意屏幕」页面,将应用发布状态从测试中修改为生产中。测试状态下颁发的refresh_token固定只有7天有效期,切换为生产状态后(个人账号自建的内部发信应用不需要提交Google审核),颁发的refresh_token即为长期有效。
- 发起授权请求时必须携带两个参数:
access_type=offline(告诉Google返回可用于刷新的refresh_token)、prompt=consent(强制显示授权确认页,确保第一次授权就能拿到refresh_token,否则Google默认不返回该字段)。 - 授权时仅申请必要权限:
https://www.googleapis.com/auth/gmail.send,不要申请多余的邮箱访问权限,降低安全风险。
第二步:实现token自动刷新与持久化,替换硬编码逻辑
你当前代码硬编码refresh_token的写法无法自动更新token,需要把token持久化存储(存到web无法访问的配置文件/数据库/缓存中),在发信时自动检测access_token有效期,过期则自动用refresh_token换取新的access_token,若刷新时Google返回了新的refresh_token(仅在安全轮换场景下出现)则自动持久化保存。
完整可落地代码如下:
<?php use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\OAuth; use League\OAuth2\Client\Provider\Google; // 配置项,建议放到独立的配置文件中 $config = [ 'clientId' => '你的Google OAuth客户端ID', 'clientSecret' => '你的Google OAuth客户端密钥', 'senderEmail' => '发信用的Gmail地址', 'tokenStoragePath' => __DIR__ . '/../secure_storage/gmail_oauth_token.json' // 必须放到web根目录外,避免被公网访问 ]; /** * 读取本地存储的OAuth凭证 */ function loadOAuthToken(string $path): array { if (!file_exists($path)) { throw new \RuntimeException("OAuth凭证文件不存在,请先完成首次授权流程"); } $content = file_get_contents($path); $token = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE || !isset($token['accessToken'])) { throw new \RuntimeException("OAuth凭证文件格式损坏"); } return $token; } /** * 保存更新后的OAuth凭证 */ function saveOAuthToken(string $path, array $token): void { file_put_contents($path, json_encode($token, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); chmod($path, 0600); // 设置严格权限,仅服务器进程可读可写 } /** * 获取有效的AccessToken,过期自动刷新 */ function getValidAccessToken(array $config, array $storedToken): array { $provider = new Google([ 'clientId' => $config['clientId'], 'clientSecret' => $config['clientSecret'] ]); // access_token未过期,提前60秒判定需要刷新,避免请求过程中失效 if (isset($storedToken['expires']) && $storedToken['expires'] > time() + 60) { return $storedToken; } // 过期则用refresh_token换新的access_token $newToken = $provider->getAccessToken('refresh_token', [ 'refresh_token' => $storedToken['refreshToken'] ]); // 组装要保存的token结构 $updatedToken = [ 'accessToken' => $newToken->getToken(), 'refreshToken' => $newToken->getRefreshToken() ?? $storedToken['refreshToken'], // 兼容Google返回新refresh_token的轮换场景 'expires' => $newToken->getExpires() ]; // 持久化保存新token saveOAuthToken($config['tokenStoragePath'], $updatedToken); return $updatedToken; } function sendEmail(array $config, string $toEmail, string $title, string $detail): ?bool { try { // 加载存储的token并自动刷新为有效状态 $storedToken = loadOAuthToken($config['tokenStoragePath']); $validToken = getValidAccessToken($config, $storedToken); $mail = new PHPMailer(true); // 这里填写你原来的SMTP配置、发件人、收件人、邮件内容等逻辑 // $mail->isSMTP(); // $mail->Host = 'smtp.gmail.com'; // $mail->SMTPAuth = true; // $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; // $mail->Port = 465; // $mail->setFrom($config['senderEmail'], '站点发信名称'); $provider = new Google([ 'clientId' => $config['clientId'], 'clientSecret' => $config['clientSecret'] ]); $mail->setOAuth(new OAuth([ 'provider' => $provider, 'clientId' => $config['clientId'], 'clientSecret' => $config['clientSecret'], 'refreshToken' => $validToken['refreshToken'], 'userName' => $config['senderEmail'], ])); $mail->addAddress($toEmail); $mail->Subject = $title; $mail->Body = $detail; return $mail->send(); } catch (\Throwable $th) { // 在这里加告警逻辑:如果异常信息包含token过期、认证失败类关键词,立即通过备用渠道(短信/企业微信/其他非Gmail邮箱)通知管理员处理 // 正常配置下该类故障触发频率极低,半年到数年才可能出现一次,管理员重新跑一次首次授权流程即可恢复 return null; } }
首次授权流程(仅需执行1次)
你需要写一个临时的授权脚本(执行完立即删除,不要长期留在生产环境),完成第一次授权拿到长期refresh_token:
- 提前创建好web根目录外的
secure_storage目录,设置权限为服务器进程可读写。 - 新建临时文件
get_gmail_token.php放到web可访问目录,代码如下:
<?php require __DIR__ . '/vendor/autoload.php'; use League\OAuth2\Client\Provider\Google; $config = [ 'clientId' => '你的Google OAuth客户端ID', 'clientSecret' => '你的Google OAuth客户端密钥', 'redirectUri' => '这个脚本的公网访问地址,比如https://你的域名/get_gmail_token.php', 'tokenSavePath' => __DIR__ . '/../secure_storage/gmail_oauth_token.json' ]; $provider = new Google([ 'clientId' => $config['clientId'], 'clientSecret' => $config['clientSecret'], 'redirectUri' => $config['redirectUri'], 'scopes' => ['https://www.googleapis.com/auth/gmail.send'], ]); if (!isset($_GET['code'])) { // 跳转Google授权页 $authUrl = $provider->getAuthorizationUrl([ 'access_type' => 'offline', 'prompt' => 'consent' ]); header("Location: {$authUrl}"); exit; } else { // 授权回调,拿到token保存 $token = $provider->getAccessToken('authorization_code', [ 'code' => $_GET['code'] ]); $tokenData = [ 'accessToken' => $token->getToken(), 'refreshToken' => $token->getRefreshToken(), 'expires' => $token->getExpires() ]; file_put_contents($config['tokenSavePath'], json_encode($tokenData, JSON_PRETTY_PRINT)); chmod($config['tokenSavePath'], 0600); echo "Token获取成功,请立即删除该临时脚本"; }
- 浏览器访问这个临时脚本的地址,登录发信用的Gmail账号完成授权,看到成功提示后立即删除该临时文件即可。
故障兜底方案
按以上配置完成后,正常发信过程中access_token会自动刷新,不需要人工干预。如果真的遇到refresh_token失效的极端场景(比如账号改密、手动撤销授权、超过6个月未发信),代码捕获到认证异常后会触发告警,你只需要重新跑一次上面的临时授权脚本即可恢复,全程耗时不超过1分钟,完全可以避免用户反馈才发现故障的问题。
内容的提问来源于stack exchange,提问作者Sittiphan Sittisak
相关产品推荐
相关产品推荐

