不使用Google Cloud PHP客户端库,PHP调用Storage API传图遇401认证错误
问题:原生PHP调用Google Cloud Storage API上传图片遇401认证错误
- 开发基于PHP的在线代码编辑器,支持用户创建包含图片的代码项目;已创建Google服务账号并获取私钥,计划用原生PHP结合cURL调用GCS API上传图片,但遇到两个核心问题:
- 无法使用Google API PHP客户端库:主机不支持Composer或Shell访问,且库体积过大,主机存储空间有限(这也是选择GCS存储图片的原因)
- 临时Access Token可成功上传,但有效期仅1小时;尝试用服务账号私钥实现持久化认证,却返回401错误:
Invalid Credentials
现有代码
// 已初始化变量 $bucketName, $projectId, $jsonKey function is_valid_image($file) { $allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']; if (!in_array($file['type'], $allowed_types)) { return false; } return true; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (isset($_FILES['image'])) { if (is_valid_image($_FILES['image'])) { $localFilePath = $_FILES['image']['tmp_name']; $objectName = $_FILES['image']['name']; $fileContent = base64_encode(file_get_contents($localFilePath)); $uploadUrl = "https://storage.googleapis.com/upload/storage/v1/b/{$bucketName}/o?uploadType=media&name={$objectName}"; $serviceAccountKey = json_decode($jsonKey); $authHeader = "Authorization: Bearer " . $serviceAccountKey['private_key']; $ch = curl_init($uploadUrl); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['name' => $objectName, 'content' => $fileContent])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', $authHeader, ]); $response = curl_exec($ch); echo $response; curl_close($ch); require "db_update.php"; } else { echo 'Invalid file type. Please upload a valid image (JPEG, JPG, PNG, GIF).'; } } else { echo 'Error uploading picture. Please try again.'; } }
返回错误信息
{ "error": { "code": 401, "message": "Invalid Credentials", "errors": [ { "message": "Invalid Credentials", "domain": "global", "reason": "authError", "locationType": "header", "location": "Authorization" } ] } }
错误原因及解决方法
核心错误点
- 直接使用私钥作为Bearer Token:服务账号私钥不能直接作为认证Token使用,必须通过私钥生成JWT(JSON Web Token),再向Google OAuth2端点换取有效的Access Token,才能用于API请求认证。
- 上传格式错误:使用
uploadType=media时,请求体应直接传递图片的原始二进制内容,而非将base64编码后的内容放在JSON结构中,同时Content-Type要匹配图片的实际类型。
修正后的完整代码
// 初始化配置(替换为你的实际信息) $bucketName = "你的存储桶名称"; $projectId = "你的Google Cloud项目ID"; $jsonKey = file_get_contents("service-account-key.json"); // 或直接赋值服务账号JSON字符串 // 验证图片类型 function is_valid_image($file) { $allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']; return in_array($file['type'], $allowed_types); } // 生成GCS认证所需的Access Token function get_gcs_access_token($jsonKey) { $keyData = json_decode($jsonKey, true); if (!$keyData) return null; // 构造JWT头部 $header = json_encode(["alg" => "RS256", "typ" => "JWT"]); // 构造JWT载荷(有效期最长12小时,这里设为1小时) $now = time(); $payload = json_encode([ "iss" => $keyData['client_email'], "scope" => "https://www.googleapis.com/auth/devstorage.full_control", // 按需调整权限 "aud" => "https://oauth2.googleapis.com/token", "exp" => $now + 3600, "iat" => $now ]); // Base64URL编码头部和载荷 $base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header)); $base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload)); // 生成签名 $signature = ""; openssl_sign($base64UrlHeader . "." . $base64UrlPayload, $signature, $keyData['private_key'], OPENSSL_ALGO_SHA256); $base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature)); // 拼接完整JWT $jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature; // 向Google换取Access Token $ch = curl_init("https://oauth2.googleapis.com/token"); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ "grant_type" => "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion" => $jwt ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); $tokenData = json_decode($response, true); return $tokenData['access_token'] ?? null; } // 处理上传请求 if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) { if (is_valid_image($_FILES['image'])) { $localFilePath = $_FILES['image']['tmp_name']; $objectName = $_FILES['image']['name']; $fileContent = file_get_contents($localFilePath); // 获取认证Token $accessToken = get_gcs_access_token($jsonKey); if (!$accessToken) { echo "获取认证Token失败"; exit; } // 构造上传URL(对文件名进行URL编码避免特殊字符问题) $uploadUrl = "https://storage.googleapis.com/upload/storage/v1/b/{$bucketName}/o?uploadType=media&name=" . urlencode($objectName); $ch = curl_init($uploadUrl); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $fileContent); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer {$accessToken}", "Content-Type: {$_FILES['image']['type']}", "Content-Length: " . filesize($localFilePath) ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 || $httpCode === 201) { echo "上传成功"; // require "db_update.php"; // 执行数据库更新逻辑 } else { echo "上传失败,响应内容:" . $response; } } else { echo '无效的文件类型,请上传JPEG、JPG、PNG或GIF格式的图片。'; } } else { echo '图片上传出错,请重试。'; } }
额外注意事项
- 权限最小化:
scope参数可按需调整,比如仅需要上传权限可改为https://www.googleapis.com/auth/devstorage.write_only,降低权限范围提升安全性。 - Token缓存:每次上传都生成Token会增加请求开销,可将生成的Token缓存到文件或数据库中,在有效期内重复使用。
- 文件名处理:建议给
$objectName添加唯一前缀(比如时间戳+随机字符串),避免同名文件被覆盖。 - 错误排查:可添加
curl_error($ch)捕获cURL请求的具体错误信息,方便调试。
内容的提问来源于stack exchange,提问作者Reem Rizk
相关产品推荐
相关产品推荐

