You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

不使用Google Cloud PHP客户端库,PHP调用Storage API传图遇401认证错误

问题:原生PHP调用Google Cloud Storage API上传图片遇401认证错误
  • 开发基于PHP的在线代码编辑器,支持用户创建包含图片的代码项目;已创建Google服务账号并获取私钥,计划用原生PHP结合cURL调用GCS API上传图片,但遇到两个核心问题:
    1. 无法使用Google API PHP客户端库:主机不支持Composer或Shell访问,且库体积过大,主机存储空间有限(这也是选择GCS存储图片的原因)
    2. 临时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" } ] } }


错误原因及解决方法

核心错误点

  1. 直接使用私钥作为Bearer Token:服务账号私钥不能直接作为认证Token使用,必须通过私钥生成JWT(JSON Web Token),再向Google OAuth2端点换取有效的Access Token,才能用于API请求认证。
  2. 上传格式错误:使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 09:34:57