HTTP 函数

如果想要通过 HTTP(S) 请求调用您的函数,可使用 HTTP 函数。HTTP 函数签名接受 HTTP 专用参数以支持 HTTP 语义。

您可以配置 HTTP 函数,使其只能通过 HTTPS 触发(如安全级别中所述)。

运行示例

要运行本文档中的示例,请确保按照快速入门中的说明为您的运行时环境设置了环境。尤其要注意,务必将示例代码库克隆到本地机器,以确保所有必需文件都存在于您的环境中。

用法示例

以下示例演示了如何处理包含 name 参数的 HTTP POST 请求:

Node.js

const escapeHtml = require('escape-html');

/**
 * HTTP Cloud Function.
 *
 * @param {Object} req Cloud Function request context.
 *                     More info: https://expressjs.com/en/api.html#req
 * @param {Object} res Cloud Function response context.
 *                     More info: https://expressjs.com/en/api.html#res
 */
exports.helloHttp = (req, res) => {
  res.send(`Hello ${escapeHtml(req.query.name || req.body.name || 'World')}!`);
};

Python

from flask import escape

def hello_http(request):
    """HTTP Cloud Function.
    Args:
        request (flask.Request): The request object.
        <https://flask.palletsprojects.com/en/1.1.x/api/#incoming-request-data>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response>.
    """
    request_json = request.get_json(silent=True)
    request_args = request.args

    if request_json and 'name' in request_json:
        name = request_json['name']
    elif request_args and 'name' in request_args:
        name = request_args['name']
    else:
        name = 'World'
    return 'Hello {}!'.format(escape(name))

Go


// Package helloworld provides a set of Cloud Functions samples.
package helloworld

import (
	"encoding/json"
	"fmt"
	"html"
	"net/http"
)

// HelloHTTP is an HTTP Cloud Function with a request parameter.
func HelloHTTP(w http.ResponseWriter, r *http.Request) {
	var d struct {
		Name string `json:"name"`
	}
	if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
		fmt.Fprint(w, "Hello, World!")
		return
	}
	if d.Name == "" {
		fmt.Fprint(w, "Hello, World!")
		return
	}
	fmt.Fprintf(w, "Hello, %s!", html.EscapeString(d.Name))
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.logging.Logger;

public class HelloHttp implements HttpFunction {
  private static final Logger logger = Logger.getLogger(HelloHttp.class.getName());

  private static final Gson gson = new Gson();

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    // Check URL parameters for "name" field
    // "world" is the default value
    String name = request.getFirstQueryParameter("name").orElse("world");

    // Parse JSON request and check for "name" field
    try {
      JsonElement requestParsed = gson.fromJson(request.getReader(), JsonElement.class);
      JsonObject requestJson = null;

      if (requestParsed != null && requestParsed.isJsonObject()) {
        requestJson = requestParsed.getAsJsonObject();
      }

      if (requestJson != null && requestJson.has("name")) {
        name = requestJson.get("name").getAsString();
      }
    } catch (JsonParseException e) {
      logger.severe("Error parsing JSON: " + e.getMessage());
    }

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Hello %s!", name);
  }
}

C#

using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;

namespace HelloHttp
{
    public class Function : IHttpFunction
    {
        private readonly ILogger _logger;

        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public async Task HandleAsync(HttpContext context)
        {
            HttpRequest request = context.Request;
            // Check URL parameters for "name" field
            // "world" is the default value
            string name = ((string) request.Query["name"]) ?? "world";

            // If there's a body, parse it as JSON and check for "name" field.
            using TextReader reader = new StreamReader(request.Body);
            string text = await reader.ReadToEndAsync();
            if (text.Length > 0)
            {
                try
                {
                    JsonElement json = JsonSerializer.Deserialize<JsonElement>(text);
                    if (json.TryGetProperty("name", out JsonElement nameElement) &&
                        nameElement.ValueKind == JsonValueKind.String)
                    {
                        name = nameElement.GetString();
                    }
                }
                catch (JsonException parseException)
                {
                    _logger.LogError(parseException, "Error parsing JSON request");
                }
            }

            await context.Response.WriteAsync($"Hello {name}!");
        }
    }
}

Ruby

require "functions_framework"
require "cgi"
require "json"

FunctionsFramework.http "hello_http" do |request|
  # The request parameter is a Rack::Request object.
  # See https://www.rubydoc.info/gems/rack/Rack/Request
  name = request.params["name"] ||
         (JSON.parse(request.body.read)["name"] rescue nil) ||
         "World"
  # Return the response body as a string.
  # You can also return a Rack::Response object, a Rack response array, or
  # a hash which will be JSON-encoded into a response.
  "Hello #{CGI.escape_html name}!"
end

PHP

<?php

use Psr\Http\Message\ServerRequestInterface;

function helloHttp(ServerRequestInterface $request): string
{
    $name = 'World';
    $body = $request->getBody()->getContents();
    if (!empty($body)) {
        $json = json_decode($body, true);
        if (json_last_error() != JSON_ERROR_NONE) {
            throw new RuntimeException(sprintf(
                'Could not parse body: %s',
                json_last_error_msg()
            ));
        }
        $name = $json['name'] ?? $name;
    }
    $queryString = $request->getQueryParams();
    $name = $queryString['name'] ?? $name;

    return sprintf('Hello, %s!', htmlspecialchars($name));
}

以下命令演示了如何使用 curl 调用函数并向其传递参数:

curl -X POST HTTP_TRIGGER_ENDPOINT -H "Content-Type:application/json"  -d '{"name":"Jane"}'

其中,HTTP_TRIGGER_ENDPOINT 是函数的网址,它是在部署函数时获取的。如需了解详情,请参阅 HTTP 触发器

自行试用

如果您是 Google Cloud 新手,请创建一个帐号来评估 Cloud Functions 在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。

免费试用 Cloud Functions

安全级别

安全级别功能用于控制 HTTP 函数的网址是仅支持 HTTPS,还是同时支持 HTTP 和 HTTPS。默认支持 HTTP 和 HTTPS。

如果您将 HTTP 函数配置为只能使用 HTTPS 来触发,则尝试使用 HTTP 协议的用户将被重定向。

您可以在部署期间指定 HTTP 函数的安全级别:

  • 如果您使用 gcloud 命令行工具来部署函数,则可以使用 --security-level 标志设置函数的安全级别。其可能值为 secure-alwayssecure-optional,这是默认值。例如:

    gcloud functions deploy FUNCTION_NAME --trigger-http --security-level=secure-always...

  • 如果您要从 Cloud Console 部署函数,请使用需要 HTTPS 复选框,使函数需要 HTTPS。

如果函数可以使用 HTTP 或 HTTPS 来触发,则函数代码可以检查请求标头 X-Forwarded-Proto 的值以确定使用的协议。安全请求将具有该标头的值 https,而其他请求将具有值 http

HTTP 框架

Cloud Functions 在每个运行时中使用特定的 HTTP 框架版本来处理 HTTP:

运行时 HTTP 框架
Node.js Express 4.17.1
Python Flask 1.0.2
Go 标准 http.HandlerFunc 接口
Java Functions 框架 Java API
.NET .NET 版 Functions 框架
Ruby Ruby 版 Functions 框架
PHP PHP 版 Functions 框架

终止 HTTP 函数

如果一个函数创建了后台任务(例如线程、future、Node.js Promise 对象、回调或系统进程),则您必须在返回 HTTP 响应之前终止或以其他方式解决这些任务。在 HTTP 响应之前未终止的任何任务可能无法完成,还可能导致未定义的行为。

解析 HTTP 请求

以下示例演示了如何读取各种格式的 HTTP 请求:

Node.js

在 Node.js 中,系统会根据 content-type 标头自动解析请求正文,并通过 HTTP 函数的参数提供该正文。

const escapeHtml = require('escape-html');

/**
 * Responds to an HTTP request using data from the request body parsed according
 * to the "content-type" header.
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.helloContent = (req, res) => {
  let name;

  switch (req.get('content-type')) {
    // '{"name":"John"}'
    case 'application/json':
      ({name} = req.body);
      break;

    // 'John', stored in a Buffer
    case 'application/octet-stream':
      name = req.body.toString(); // Convert buffer to a string
      break;

    // 'John'
    case 'text/plain':
      name = req.body;
      break;

    // 'name=John' in the body of a POST request (not the URL)
    case 'application/x-www-form-urlencoded':
      ({name} = req.body);
      break;
  }

  res.status(200).send(`Hello ${escapeHtml(name || 'World')}!`);
};

Python

from flask import escape

def hello_content(request):
    """ Responds to an HTTP request using data from the request body parsed
    according to the "content-type" header.
    Args:
        request (flask.Request): The request object.
        <https://flask.palletsprojects.com/en/1.1.x/api/#incoming-request-data>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response>.
    """
    content_type = request.headers['content-type']
    if content_type == 'application/json':
        request_json = request.get_json(silent=True)
        if request_json and 'name' in request_json:
            name = request_json['name']
        else:
            raise ValueError("JSON is invalid, or missing a 'name' property")
    elif content_type == 'application/octet-stream':
        name = request.data
    elif content_type == 'text/plain':
        name = request.data
    elif content_type == 'application/x-www-form-urlencoded':
        name = request.form.get('name')
    else:
        raise ValueError("Unknown content type: {}".format(content_type))
    return 'Hello {}!'.format(escape(name))

Go


// Package http provides a set of HTTP Cloud Functions samples.
package http

import (
	"encoding/json"
	"fmt"
	"html"
	"io/ioutil"
	"log"
	"net/http"
)

// HelloContentType is an HTTP Cloud function.
// It uses the Content-Type header to identify the request payload format.
func HelloContentType(w http.ResponseWriter, r *http.Request) {
	var name string

	switch r.Header.Get("Content-Type") {
	case "application/json":
		var d struct {
			Name string `json:"name"`
		}
		err := json.NewDecoder(r.Body).Decode(&d)
		if err != nil {
			log.Printf("error parsing application/json: %v", err)
		} else {
			name = d.Name
		}
	case "application/octet-stream":
		body, err := ioutil.ReadAll(r.Body)
		if err != nil {
			log.Printf("error parsing application/octet-stream: %v", err)
		} else {
			name = string(body)
		}
	case "text/plain":
		body, err := ioutil.ReadAll(r.Body)
		if err != nil {
			log.Printf("error parsing text/plain: %v", err)
		} else {
			name = string(body)
		}
	case "application/x-www-form-urlencoded":
		if err := r.ParseForm(); err != nil {
			log.Printf("error parsing application/x-www-form-urlencoded: %v", err)
		} else {
			name = r.FormValue("name")
		}
	}

	if name == "" {
		name = "World"
	}

	fmt.Fprintf(w, "Hello, %s!", html.EscapeString(name))
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Optional;

public class ParseContentType implements HttpFunction {

  // Use GSON (https://github.com/google/gson) to parse JSON content.
  private static final Gson gson = new Gson();

  // Responds to an HTTP request using data from the request body parsed according to the
  // "content-type" header.
  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    String name = null;

    // Default values avoid null issues (with switch/case) and exceptions from get() (optionals)
    String contentType = request.getContentType().orElse("");

    switch (contentType) {
      case "application/json":
        // '{"name":"John"}'
        JsonObject body = gson.fromJson(request.getReader(), JsonObject.class);
        if (body.has("name")) {
          name = body.get("name").getAsString();
        }
        break;
      case "application/octet-stream":
        // 'John', stored in a Buffer
        name = new String(Base64.getDecoder().decode(request.getInputStream().readAllBytes()),
            StandardCharsets.UTF_8);
        break;
      case "text/plain":
        // 'John'
        name = request.getReader().readLine();
        break;
      case "application/x-www-form-urlencoded":
        // 'name=John' in the body of a POST request (not the URL)
        Optional<String> nameParam = request.getFirstQueryParameter("name");
        if (nameParam.isPresent()) {
          name = nameParam.get();
        }
        break;
      default:
        // Invalid or missing "Content-Type" header
        response.setStatusCode(HttpURLConnection.HTTP_UNSUPPORTED_TYPE);
        return;
    }

    // Verify that a name was provided
    if (name == null) {
      response.setStatusCode(HttpURLConnection.HTTP_BAD_REQUEST);
    }

    // Respond with a name
    var writer = new PrintWriter(response.getWriter());
    writer.printf("Hello %s!", name);
  }
}

C#

using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace ParseContentType
{
    public class Function : IHttpFunction
    {
        public async Task HandleAsync(HttpContext context)
        {
            HttpRequest request = context.Request;
            HttpResponse response = context.Response;

            string name = null;
            ContentType contentType = new ContentType(request.ContentType);

            switch (contentType.MediaType)
            {
                case "application/json":
                {
                    // '{"name":"John"}'
                    using TextReader reader = new StreamReader(request.Body);
                    string json = await reader.ReadToEndAsync();
                    JsonElement body = JsonSerializer.Deserialize<JsonElement>(json);
                    if (body.TryGetProperty("name", out JsonElement property) && property.ValueKind == JsonValueKind.String)
                    {
                        name = property.GetString();
                    }
                    break;
                }
                case "application/octet-stream":
                {
                    // 'John', encoded to bytes using UTF-8, then encoded as base64
                    using TextReader reader = new StreamReader(request.Body);
                    string base64 = await reader.ReadToEndAsync();
                    byte[] data = Convert.FromBase64String(base64);
                    name = Encoding.UTF8.GetString(data);
                    break;
                }
                case "text/plain":
                {
                    // 'John'
                    using TextReader reader = new StreamReader(request.Body);
                    name = await reader.ReadLineAsync();
                    break;
                }
                case "application/x-www-form-urlencoded":
                {
                    // 'name=John' in the body of a POST request (not the URL)
                    if (request.Form.TryGetValue("name", out StringValues value))
                    {
                        name = value;
                    }
                    break;
                }
            }
            if (name is object)
            {
                await response.WriteAsync($"Hello {name}!");
            }
            else
            {
                // Unrecognized content type, or the name wasn't in the content
                // (e.g. JSON without a "name" property)
                response.StatusCode = (int) HttpStatusCode.BadRequest;
            }
        }
    }
}

Ruby

require "functions_framework"
require "json"

FunctionsFramework.http "http_content" do |request|
  # The request parameter is a Rack::Request object.
  # See https://www.rubydoc.info/gems/rack/Rack/Request
  content_type = request.content_type
  case content_type
  # '{"name":"John"}'
  when "application/json"
    name = (JSON.parse(request.body.read.to_s)["name"] rescue nil)
  # "John", stored in a Buffer
  when "application/octet-stream"
    name = request.body.read.to_s # Convert buffer to a string
  # "John"
  when "text/plain"
    name = request.body.read.to_s
  # "name=John" in the body of a POST request (not the URL)
  when "application/x-www-form-urlencoded"
    name = (request.params["name"] rescue nil)
  end

  name ||= "World"

  # Return the response body as a string.
  # You can also return a Rack::Response object, a Rack response array, or
  # a hash which will be JSON-encoded into a response.
  "Hello #{name}!"
end

PHP


use Psr\Http\Message\ServerRequestInterface;

function helloContent(ServerRequestInterface $request): string
{
    $name = 'World';
    $body = $request->getBody()->getContents();
    switch ($request->getHeaderLine('content-type')) {
        // '{"name":"John"}'
        case 'application/json':
          if (!empty($body)) {
              $json = json_decode($body, true);
              if (json_last_error() != JSON_ERROR_NONE) {
                  throw new RuntimeException(sprintf(
                      'Could not parse body: %s',
                      json_last_error_msg()
                  ));
              }
              $name = $json['name'] ?? $name;
          }
          break;
        // 'John', stored in a stream
        case 'application/octet-stream':
          $name = $body;
          break;
        // 'John'
        case 'text/plain':
          $name = $body;
          break;
        // 'name=John' in the body of a POST request (not the URL)
        case 'application/x-www-form-urlencoded':
          parse_str($body, $data);
          $name = $data['name'] ?? $name;
          break;
    }

    return sprintf('Hello %s!', htmlspecialchars($name));
}

处理 CORS 请求

跨域资源共享 (CORS) 的作用是让在一个网域上运行的应用访问另一网域中的内容,例如,让 yourdomain.comregion-project.cloudfunctions.net/yourfunction 发出请求。

如果 CORS 设置得不正确,则可能会出现如下错误:

XMLHttpRequest cannot load https://region-project.cloudfunctions.net/function.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'http://yourdomain.com' is therefore not allowed access.

预检请求

CORS 包含两个请求:

  • 预检 OPTIONS 请求。
  • 主请求(在 OPTIONS 请求之后)。

预检请求包含标头,用于指示将在主请求中发送哪种方法 (Access-Control-Request-Method) 和哪些附加标头 (Access-Control-Request-Headers),以及主请求的来源 (Origin)。

要处理预检请求,您必须设置相应的 Access-Control-Allow-* 标头以匹配要接受的请求:

Node.js

/**
 * HTTP function that supports CORS requests.
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.corsEnabledFunction = (req, res) => {
  // Set CORS headers for preflight requests
  // Allows GETs from any origin with the Content-Type header
  // and caches preflight response for 3600s

  res.set('Access-Control-Allow-Origin', '*');

  if (req.method === 'OPTIONS') {
    // Send response to OPTIONS requests
    res.set('Access-Control-Allow-Methods', 'GET');
    res.set('Access-Control-Allow-Headers', 'Content-Type');
    res.set('Access-Control-Max-Age', '3600');
    res.status(204).send('');
  } else {
    res.send('Hello World!');
  }
};

Python

def cors_enabled_function(request):
    # For more information about CORS and CORS preflight requests, see:
    # https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request

    # Set CORS headers for the preflight request
    if request.method == 'OPTIONS':
        # Allows GET requests from any origin with the Content-Type
        # header and caches preflight response for an 3600s
        headers = {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'GET',
            'Access-Control-Allow-Headers': 'Content-Type',
            'Access-Control-Max-Age': '3600'
        }

        return ('', 204, headers)

    # Set CORS headers for the main request
    headers = {
        'Access-Control-Allow-Origin': '*'
    }

    return ('Hello World!', 200, headers)

Go


// Package http provides a set of HTTP Cloud Functions samples.
package http

import (
	"fmt"
	"net/http"
)

// CORSEnabledFunction is an example of setting CORS headers.
// For more information about CORS and CORS preflight requests, see
// https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request.
func CORSEnabledFunction(w http.ResponseWriter, r *http.Request) {
	// Set CORS headers for the preflight request
	if r.Method == http.MethodOptions {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "POST")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
		w.Header().Set("Access-Control-Max-Age", "3600")
		w.WriteHeader(http.StatusNoContent)
		return
	}
	// Set CORS headers for the main request.
	w.Header().Set("Access-Control-Allow-Origin", "*")
	fmt.Fprint(w, "Hello, World!")
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.BufferedWriter;
import java.io.IOException;
import java.net.HttpURLConnection;

public class CorsEnabled implements HttpFunction {
  // corsEnabled is an example of setting CORS headers.
  // For more information about CORS and CORS preflight requests, see
  // https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request.
  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    // Set CORS headers
    //   Allows GETs from any origin with the Content-Type
    //   header and caches preflight response for 3600s
    response.appendHeader("Access-Control-Allow-Origin", "*");

    if ("OPTIONS".equals(request.getMethod())) {
      response.appendHeader("Access-Control-Allow-Methods", "GET");
      response.appendHeader("Access-Control-Allow-Headers", "Content-Type");
      response.appendHeader("Access-Control-Max-Age", "3600");
      response.setStatusCode(HttpURLConnection.HTTP_NO_CONTENT);
      return;
    }

    // Handle the main request.
    BufferedWriter writer = response.getWriter();
    writer.write("CORS headers set successfully!");
  }
}

C#

using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using System.Net;
using System.Threading.Tasks;

namespace Cors
{
    // For more information about CORS and CORS preflight requests, see
    // https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request.
    public class Function : IHttpFunction
    {
        public async Task HandleAsync(HttpContext context)
        {
            HttpRequest request = context.Request;
            HttpResponse response = context.Response;

            // Set CORS headers
            //   Allows GETs from any origin with the Content-Type
            //   header and caches preflight response for 3600s

            response.Headers.Append("Access-Control-Allow-Origin", "*");
            if (HttpMethods.IsOptions(request.Method))
            {
                response.Headers.Append("Access-Control-Allow-Methods", "GET");
                response.Headers.Append("Access-Control-Allow-Headers", "Content-Type");
                response.Headers.Append("Access-Control-Max-Age", "3600");
                response.StatusCode = (int) HttpStatusCode.NoContent;
                return;
            }

            await response.WriteAsync("CORS headers set successfully!");
        }
    }
}

Ruby

FunctionsFramework.http "cors_enabled_function" do |request|
  # For more information about CORS and CORS preflight requests, see
  # https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
  # for more information.

  # Set CORS headers for the preflight request
  if request.options?
    # Allows GET requests from any origin with the Content-Type
    # header and caches preflight response for an 3600s
    headers = {
      "Access-Control-Allow-Origin"  => "*",
      "Access-Control-Allow-Methods" => "GET",
      "Access-Control-Allow-Headers" => "Content-Type",
      "Access-Control-Max-Age"       => "3600"
    }
    [204, headers, []]
  else
    # Set CORS headers for the main request
    headers = {
      "Access-Control-Allow-Origin" => "*"
    }

    [200, headers, ["Hello World!"]]
  end
end

PHP


use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use GuzzleHttp\Psr7\Response;

function corsEnabledFunction(ServerRequestInterface $request): ResponseInterface
{
    // Set CORS headers for preflight requests
    // Allows GETs from any origin with the Content-Type header
    // and caches preflight response for 3600s
    $headers = ['Access-Control-Allow-Origin' => '*'];

    if ($request->getMethod() === 'OPTIONS') {
        // Send response to OPTIONS requests
        $headers = array_merge($headers, [
            'Access-Control-Allow-Methods' => 'GET',
            'Access-Control-Allow-Headers' => 'Content-Type',
            'Access-Control-Max-Age' => '3600'
        ]);
        return new Response(204, $headers, '');
    } else {
        return new Response(200, $headers, 'Hello World!');
    }
}

此外,您还可以使用第三方库为您处理 CORS。

限制

CORS 预检请求是在没有 Authorization 标头的情况下发送的,因此所有非公开 HTTP 函数都将拒绝这些请求。由于预检请求失败,因此主请求也将失败。

HTTP 函数默认需要进行身份验证,因此以下解决方法可帮助您应对此限制:

在同一网域上托管

您可以在同一网域中托管您的网站和函数,以避免实现 CORS。如此一来,请求将来自同一来源,因此系统将不会强制实施 CORS。这大大简化了您的代码。

最简单的方法是将 Firebase 托管与 Google Cloud Functions 函数进行集成

使用 Cloud Endpoints 处理 CORS

您可以部署 Cloud Endpoints 代理启用 CORS

如果您需要身份验证功能,还可以启用 Google ID 令牌验证,以验证身份验证令牌。

处理 HTTP 方法

HTTP 函数接受所有 HTTP 方法。以下示例演示了如何根据收到的 HTTP 方法(例如,GETPUT)执行不同的操作:

Node.js

/**
 * Responds to a GET request with "Hello World!". Forbids a PUT request.
 *
 * @example
 * gcloud functions call helloHttp
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.helloHttp = (req, res) => {
  switch (req.method) {
    case 'GET':
      res.status(200).send('Hello World!');
      break;
    case 'PUT':
      res.status(403).send('Forbidden!');
      break;
    default:
      res.status(405).send({error: 'Something blew up!'});
      break;
  }
};

Python

def hello_method(request):
    """ Responds to a GET request with "Hello world!". Forbids a PUT request.
    Args:
        request (flask.Request): The request object.
        <https://flask.palletsprojects.com/en/1.1.x/api/#incoming-request-data>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response>.
    """
    from flask import abort

    if request.method == 'GET':
        return 'Hello World!'
    elif request.method == 'PUT':
        return abort(403)
    else:
        return abort(405)

Go


// Package http provides a set of HTTP Cloud Functions samples.
package http

import (
	"fmt"
	"net/http"
)

// HelloHTTPMethod is an HTTP Cloud function.
// It uses the request method to differentiate the response.
func HelloHTTPMethod(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		fmt.Fprint(w, "Hello World!")
	case http.MethodPut:
		http.Error(w, "403 - Forbidden", http.StatusForbidden)
	default:
		http.Error(w, "405 - Method Not Allowed", http.StatusMethodNotAllowed)
	}
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.BufferedWriter;
import java.io.IOException;
import java.net.HttpURLConnection;

public class HttpMethod implements HttpFunction {
  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {

    BufferedWriter writer = response.getWriter();

    switch (request.getMethod()) {
      case "GET":
        response.setStatusCode(HttpURLConnection.HTTP_OK);
        writer.write("Hello world!");
        break;
      case "PUT":
        response.setStatusCode(HttpURLConnection.HTTP_FORBIDDEN);
        writer.write("Forbidden!");
        break;
      default:
        response.setStatusCode(HttpURLConnection.HTTP_BAD_METHOD);
        writer.write("Something blew up!");
        break;
    }
  }
}

C#

using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using System.Net;
using System.Threading.Tasks;

namespace HttpRequestMethod
{
    public class Function : IHttpFunction
    {
        public async Task HandleAsync(HttpContext context)
        {
            HttpResponse response = context.Response;
            switch (context.Request.Method)
            {
                case "GET":
                    response.StatusCode = (int) HttpStatusCode.OK;
                    await response.WriteAsync("Hello world!");
                    break;
                case "PUT":
                    response.StatusCode = (int) HttpStatusCode.Forbidden;
                    await response.WriteAsync("Forbidden!");
                    break;
                default:
                    response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
                    await response.WriteAsync("Something blew up!");
                    break;
            }
        }
    }
}

Ruby

require "functions_framework"
require "json"

FunctionsFramework.http "http_method" do |request|
  # The request parameter is a Rack::Request object.
  # See https://www.rubydoc.info/gems/rack/Rack/Request
  case request.request_method
  when "GET"
    status = 200
    body = "Hello World!"
  when "PUT"
    status = 403
    body = "Forbidden!"
  else
    status = 405
    body = '{"error":"Something blew up!"}'
  end

  # Return the response body as a Rack::Response object.
  ::Rack::Response.new body, status
end

PHP


use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use GuzzleHttp\Psr7\Response;

function httpMethod(ServerRequestInterface $request): ResponseInterface
{
    switch ($request->getMethod()) {
        case 'GET':
            // Example: read request
            return new Response(
                200, // OK
                [],
                'Hello, World!' . PHP_EOL
            );
            break;
        case 'PUT':
            // Example: write request to a read-only resource
            return new Response(
                403, // Permission denied
                [],
                'Forbidden!' . PHP_EOL
            );
            break;
        default:
            // Example: request type not supported by the application
            $json_payload = json_encode([
                'error' => 'something blew up!'
            ]);
            return new Response(
                405, // Method not allowed
                ['Content-Type' => 'application/json'],
                $json_payload
            );
            break;
    }
}

处理内容类型

对于 Node.js,Cloud Functions 函数会解析内容类型为 application/jsonapplication/x-www-form-urlencoded 的请求正文(如上所示)。类型为纯文本 (text/plain) 的内容是以字符串的形式传递的,采用 UTF-8(或 content-type 标头中提供的自定义编码格式)作为默认编码格式。

要访问其他类型的内容,检查 HTTP 函数的参数即可。 此操作的方法因语言而异。

以下示例处理的是内容类型为 text/xml 的请求:

Node.js

rawBody 属性包含请求正文的未解析字节。

/**
 * Parses a document of type 'text/xml'
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.parseXML = (req, res) => {
  // Convert the request to a Buffer and a string
  // Use whichever one is accepted by your XML parser
  const data = req.rawBody;
  const xmlData = data.toString();

  const {parseString} = require('xml2js');

  parseString(xmlData, (err, result) => {
    if (err) {
      console.error(err);
      res.status(500).end();
      return;
    }
    res.send(result);
  });
};

Python

import json
import xmltodict

def parse_xml(request):
    """ Parses a document of type 'text/xml'
    Args:
        request (flask.Request): The request object.
    Returns:
        The response text, or any set of values that can be turned into a
         Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """
    data = xmltodict.parse(request.data)
    return json.dumps(data, indent=2)

Go


// Package http provides a set of HTTP Cloud Functions samples.
package http

import (
	"encoding/xml"
	"fmt"
	"html"
	"io/ioutil"
	"net/http"
)

// ParseXML is an example of parsing a text/xml request.
func ParseXML(w http.ResponseWriter, r *http.Request) {
	var d struct {
		Name string
	}
	b, err := ioutil.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Could not read request", http.StatusBadRequest)
	}
	if err := xml.Unmarshal(b, &d); err != nil {
		http.Error(w, "Could not parse request", http.StatusBadRequest)
	}
	if d.Name == "" {
		d.Name = "World"
	}
	fmt.Fprintf(w, "Hello, %v!", html.EscapeString(d.Name))
}

分段数据

以下示例演示了如何处理内容类型为 multipart/form-data 的数据。根据您选择的语言,您可能需要使用解析库。

Node.js

/**
 * Parses a 'multipart/form-data' upload request
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
const path = require('path');
const os = require('os');
const fs = require('fs');

// Node.js doesn't have a built-in multipart/form-data parsing library.
// Instead, we can use the 'busboy' library from NPM to parse these requests.
const Busboy = require('busboy');

exports.uploadFile = (req, res) => {
  if (req.method !== 'POST') {
    // Return a "method not allowed" error
    return res.status(405).end();
  }
  const busboy = new Busboy({headers: req.headers});
  const tmpdir = os.tmpdir();

  // This object will accumulate all the fields, keyed by their name
  const fields = {};

  // This object will accumulate all the uploaded files, keyed by their name.
  const uploads = {};

  // This code will process each non-file field in the form.
  busboy.on('field', (fieldname, val) => {
    /**
     *  TODO(developer): Process submitted field values here
     */
    console.log(`Processed field ${fieldname}: ${val}.`);
    fields[fieldname] = val;
  });

  const fileWrites = [];

  // This code will process each file uploaded.
  busboy.on('file', (fieldname, file, filename) => {
    // Note: os.tmpdir() points to an in-memory file system on GCF
    // Thus, any files in it must fit in the instance's memory.
    console.log(`Processed file ${filename}`);
    const filepath = path.join(tmpdir, filename);
    uploads[fieldname] = filepath;

    const writeStream = fs.createWriteStream(filepath);
    file.pipe(writeStream);

    // File was processed by Busboy; wait for it to be written.
    // Note: GCF may not persist saved files across invocations.
    // Persistent files must be kept in other locations
    // (such as Cloud Storage buckets).
    const promise = new Promise((resolve, reject) => {
      file.on('end', () => {
        writeStream.end();
      });
      writeStream.on('finish', resolve);
      writeStream.on('error', reject);
    });
    fileWrites.push(promise);
  });

  // Triggered once all uploaded files are processed by Busboy.
  // We still need to wait for the disk writes (saves) to complete.
  busboy.on('finish', async () => {
    await Promise.all(fileWrites);

    /**
     * TODO(developer): Process saved files here
     */
    for (const file in uploads) {
      fs.unlinkSync(uploads[file]);
    }
    res.send();
  });

  busboy.end(req.rawBody);
};

Python

import os
import tempfile
from werkzeug.utils import secure_filename

# Helper function that computes the filepath to save files to
def get_file_path(filename):
    # Note: tempfile.gettempdir() points to an in-memory file system
    # on GCF. Thus, any files in it must fit in the instance's memory.
    file_name = secure_filename(filename)
    return os.path.join(tempfile.gettempdir(), file_name)

def parse_multipart(request):
    """ Parses a 'multipart/form-data' upload request
    Args:
        request (flask.Request): The request object.
    Returns:
        The response text, or any set of values that can be turned into a
         Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """

    # This code will process each non-file field in the form
    fields = {}
    data = request.form.to_dict()
    for field in data:
        fields[field] = data[field]
        print('Processed field: %s' % field)

    # This code will process each file uploaded
    files = request.files.to_dict()
    for file_name, file in files.items():
        # Note: GCF may not keep files saved locally between invocations.
        # If you want to preserve the uploaded files, you should save them
        # to another location (such as a Cloud Storage bucket).
        file.save(get_file_path(file_name))
        print('Processed file: %s' % file_name)

    # Clear temporary directory
    for file_name in files:
        file_path = get_file_path(file_name)
        os.remove(file_path)

    return "Done!"

Go


// Package http provides a set of HTTP Cloud Functions samples.
package http

import (
	"fmt"
	"log"
	"net/http"
)

// UploadFile processes a 'multipart/form-data' upload request.
func UploadFile(w http.ResponseWriter, r *http.Request) {
	const maxMemory = 2 * 1024 * 1024 // 2 megabytes.

	// ParseMultipartForm parses a request body as multipart/form-data.
	// The whole request body is parsed and up to a total of maxMemory bytes of
	// its file parts are stored in memory, with the remainder stored on
	// disk in temporary files.

	// Note that any files saved during a particular invocation may not
	// persist after the current invocation completes; persistent files
	// should be stored elsewhere, such as in a Cloud Storage bucket.
	if err := r.ParseMultipartForm(maxMemory); err != nil {
		http.Error(w, "Unable to parse form", http.StatusBadRequest)
		log.Printf("Error parsing form: %v", err)
		return
	}

	// Be sure to remove all temporary files after your function is finished.
	defer func() {
		if err := r.MultipartForm.RemoveAll(); err != nil {
			http.Error(w, "Error cleaning up form files", http.StatusInternalServerError)
			log.Printf("Error cleaning up form files: %v", err)
		}
	}()

	// r.MultipartForm.File contains *multipart.FileHeader values for every
	// file in the form. You can access the file contents using
	// *multipart.FileHeader's Open method.
	for _, headers := range r.MultipartForm.File {
		for _, h := range headers {
			fmt.Fprintf(w, "File uploaded: %q (%v bytes)", h.Filename, h.Size)
			// Use h.Open() to read the contents of the file.
		}
	}

}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.logging.Logger;

public class HttpFormData implements HttpFunction {
  private static final Logger logger = Logger.getLogger(HttpFormData.class.getName());

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {

    if (!"POST".equals(request.getMethod())) {
      response.setStatusCode(HttpURLConnection.HTTP_BAD_METHOD);
      return;
    }

    // This code will process each file uploaded.
    String tempDirectory = System.getProperty("java.io.tmpdir");
    for (HttpRequest.HttpPart httpPart : request.getParts().values()) {
      String filename = httpPart.getFileName().orElse(null);
      if (filename == null) {
        continue;
      }

      logger.info("Processed file: " + filename);

      // Note: GCF's temp directory is an in-memory file system
      // Thus, any files in it must fit in the instance's memory.
      Path filePath = Paths.get(tempDirectory, filename).toAbsolutePath();

      // Note: files saved to a GCF instance itself may not persist across executions.
      // Persistent files should be stored elsewhere, e.g. a Cloud Storage bucket.
      Files.copy(httpPart.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);

      // TODO(developer): process saved files here
      Files.delete(filePath);
    }

    // This code will process other form fields.
    request.getQueryParameters().forEach(
        (fieldName, fieldValues) -> {
          String firstFieldValue = fieldValues.get(0);

          // TODO(developer): process field values here
          logger.info(String.format(
              "Processed field: %s (value: %s)", fieldName, firstFieldValue));
        });
  }
}

C#

using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;

namespace HttpFormData
{
    public class Function : IHttpFunction
    {
        private readonly ILogger _logger;

        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public async Task HandleAsync(HttpContext context)
        {
            HttpResponse response = context.Response;
            HttpRequest request = context.Request;

            if (request.Method != "POST")
            {
                response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
                return;
            }

            // This code will process each file uploaded.
            string tempDirectory = Path.GetTempPath();
            foreach (IFormFile file in request.Form.Files)
            {
                if (string.IsNullOrEmpty(file.FileName))
                {
                    continue;
                }
                _logger.LogInformation("Processed file: {file}", file.FileName);

                // Note: GCF's temp directory is an in-memory file system
                // Thus, any files in it must fit in the instance's memory.
                string outputPath = Path.Combine(tempDirectory, file.FileName);

                // Note: files saved to a GCF instance itself may not persist across executions.
                // Persistent files should be stored elsewhere, e.g. a Cloud Storage bucket.
                using (FileStream output = File.Create(outputPath))
                {
                    await file.CopyToAsync(output);
                }

                // TODO(developer): process saved files here
                File.Delete(outputPath);
            }

            // This code will process other form fields.
            foreach (KeyValuePair<string, StringValues> parameter in request.Form)
            {
                // TODO(developer): process field values here
                _logger.LogInformation("Processed field '{key}' (value: '{value}')",
                    parameter.Key, (string) parameter.Value);
            }
        }
    }
}

Ruby

require "functions_framework"

FunctionsFramework.http "http_form_data" do |request|
  # The request parameter is a Rack::Request object.
  # See https://www.rubydoc.info/gems/rack/Rack/Request

  # This Rack call parses multipart form data, returning the params as a hash.
  # The returned params hash includes an entry for each field. File uploads
  # are written to Tempfiles and represented by hashes, while plain fields are
  # represented by strings.
  params = request.POST

  begin
    params.each do |name, part|
      if part.is_a? Hash
        # Handle a file upload part by logging the md5 hash.
        md5 = Digest::MD5.hexdigest part[:tempfile].read
        file_name = part[:filename]
        logger.info "Processed file=#{file_name} md5=#{md5}"
      else
        # Handle a non-file part by logging the value.
        logger.info "Processed field=#{name} value=#{part}"
      end
    end
  ensure
    # Ensure that all Tempfile objects are closed and deleted. The Cloud
    # Functions runtime keeps temporary files in an in-memory file system,
    # so to lower memory usage it is good practice to clean up Tempfiles
    # explicitly rather than wait for object finalization.
    params.each_value do |part|
      part[:tempfile].close! if part.is_a? Hash
    end
  end

  # The HTTP response body.
  "OK"
end

PHP


use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Psr7\Response;

function uploadFile(ServerRequestInterface $request): ResponseInterface
{
    if ($request->getMethod() != 'POST') {
        return new Response(405, [], 'Method Not Allowed: expected POST, found ' . $request->getMethod());
    }

    $contentType = $request->getHeader('Content-Type')[0];
    if (strpos($contentType, 'multipart/form-data') !== 0) {
        return new Response(400, [], 'Bad Request: content of type "multipart/form-data" not provided, found ' . $contentType);
    }

    $fileList = [];
    /** @var $file Psr\Http\Message\UploadedFileInterface */
    foreach ($request->getUploadedFiles() as $name => $file) {
        // Use caution when trusting the client-provided filename:
        // https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload
        $fileList[] = $file->getClientFilename();

        infoLog('Processing ' . $file->getClientFilename());
        $filename = tempnam(sys_get_temp_dir(), $name . '.') . '-' . $file->getClientFilename();

        // Use $file->getStream() to process the file contents in ways other than a direct "file save".
        infoLog('Saving to ' . $filename);
        $file->moveTo($filename);
    }

    if (empty($fileList)) {
        $msg = 'Bad Request: no files sent for upload';
        errorLog($msg);
        return new Response(400, [], $msg);
    }

    return new Response(201, [], 'Saved ' . join(', ', $fileList));
}

function errorLog($msg): void
{
    $stream = fopen('php://stderr', 'wb');
    $entry = json_encode(['msg' => $msg, 'severity' => 'error'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    fwrite($stream, $entry . PHP_EOL);
}

function infoLog($msg): void
{
    $stream = fopen('php://stderr', 'wb');
    $entry = json_encode(['message' => $msg, 'severity' => 'info'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    fwrite($stream, $entry . PHP_EOL);
}

通过 Cloud Storage 上传文件

Cloud Functions 常见的使用场景是文件处理。对于较大文件或需要的永久性存储空间超出单个请求范围的文件,可以使用 Cloud Storage 作为文件上传的入口点。如需完成此操作,您必须生成一个签名网址以提供对 Cloud Storage 存储分区的临时写入访问权限。

如果您要直接使用 Cloud Functions 函数,可使用适当的 Cloud Storage 客户端库生成一个签名网址。

使用 Cloud Storage 将文件上传到 Cloud Functions 函数的过程分为三步:

  1. 客户端直接调用 Cloud Functions 函数来检索签名网址。

  2. 然后,客户端通过 HTTP PUT 请求将文件数据发送到签名网址。

  3. 第二个 Cloud Functions 函数被存储分区中的变更触发,以对文件进行进一步处理。

以下示例演示了如何使用 Cloud Storage 客户端库生成签名网址。

Cloud Functions 函数具有“默认应用凭据”,该凭据通常不含 iam.serviceAccounts.signBlob 权限。要获取此权限,您需要先确保您的函数的服务帐号拥有适当的角色。您可以使用 Cloud Console 或 gcloud 命令行工具来实现此目的:

控制台

要确保您的函数的服务帐号拥有适当角色,您可以直接修改帐号的 IAM 角色:

  1. 转到 Google Cloud Console:

    转到 Google Cloud Console

  2. 选择相应帐号,然后依次选择 Editor > 服务帐号 > Service Account Token Creator

gcloud

要确保您的函数的服务帐号拥有适当角色,请运行以下命令。预定义的 serviceAccountTokenCreator 角色具有您需要的 iam.serviceAccounts.signBlob 权限:

gcloud projects add-iam-policy-binding YOUR_PROJECT \
--member serviceAccount:YOUR_SERVICE_ACCOUNT --role roles/iam.serviceAccountTokenCreator

您可以使用 Cloud Console 或 gcloud 命令行工具来确定您的函数使用的服务帐号:

控制台

要使用 Cloud Console 确定您的函数使用的服务帐号,请执行以下操作:

  1. 转到 Google Cloud Console:

    转到 Google Cloud Console

  2. 从列表中选择要检查的函数。

您可以在函数的详细信息页面上查看服务帐号。

gcloud

要确定您的函数使用的服务帐号,请运行以下命令并查找 serviceAccountEmail 属性:

gcloud beta functions describe YOUR_FUNCTION_NAME

以下是一个生成签名网址的示例:

Node.js

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// The ID of your GCS bucket
// const bucketName = 'your-unique-bucket-name';

// The full path of your file inside the GCS bucket, e.g. 'yourFile.jpg' or 'folder1/folder2/yourFile.jpg'
// const fileName = 'your-file-name';

// Imports the Google Cloud client library
const {Storage} = require('@google-cloud/storage');

// Creates a client
const storage = new Storage();

async function generateV4UploadSignedUrl() {
  // These options will allow temporary uploading of the file with outgoing
  // Content-Type: application/octet-stream header.
  const options = {
    version: 'v4',
    action: 'write',
    expires: Date.now() + 15 * 60 * 1000, // 15 minutes
    contentType: 'application/octet-stream',
  };

  // Get a v4 signed URL for uploading file
  const [url] = await storage
    .bucket(bucketName)
    .file(fileName)
    .getSignedUrl(options);

  console.log('Generated PUT signed URL:');
  console.log(url);
  console.log('You can use this URL with any user agent, for example:');
  console.log(
    "curl -X PUT -H 'Content-Type: application/octet-stream' " +
      `--upload-file my-file '${url}'`
  );
}

generateV4UploadSignedUrl().catch(console.error);

Python

import datetime

from google.cloud import storage

def generate_upload_signed_url_v4(bucket_name, blob_name):
    """Generates a v4 signed URL for uploading a blob using HTTP PUT.

    Note that this method requires a service account key file. You can not use
    this if you are using Application Default Credentials from Google Compute
    Engine or from the Google Cloud SDK.
    """
    # bucket_name = 'your-bucket-name'
    # blob_name = 'your-object-name'

    storage_client = storage.Client()
    bucket = storage_client.bucket(bucket_name)
    blob = bucket.blob(blob_name)

    url = blob.generate_signed_url(
        version="v4",
        # This URL is valid for 15 minutes
        expiration=datetime.timedelta(minutes=15),
        # Allow PUT requests using this URL.
        method="PUT",
        content_type="application/octet-stream",
    )

    print("Generated PUT signed URL:")
    print(url)
    print("You can use this URL with any user agent, for example:")
    print(
        "curl -X PUT -H 'Content-Type: application/octet-stream' "
        "--upload-file my-file '{}'".format(url)
    )
    return url

Go

import (
	"fmt"
	"io"
	"io/ioutil"
	"time"

	"cloud.google.com/go/storage"
	"golang.org/x/oauth2/google"
)

// generateV4PutObjectSignedURL generates object signed URL with PUT method.
func generateV4PutObjectSignedURL(w io.Writer, bucket, object, serviceAccount string) (string, error) {
	// bucket := "bucket-name"
	// object := "object-name"
	// serviceAccount := "service_account.json"
	jsonKey, err := ioutil.ReadFile(serviceAccount)
	if err != nil {
		return "", fmt.Errorf("ioutil.ReadFile: %v", err)
	}
	conf, err := google.JWTConfigFromJSON(jsonKey)
	if err != nil {
		return "", fmt.Errorf("google.JWTConfigFromJSON: %v", err)
	}
	opts := &storage.SignedURLOptions{
		Scheme: storage.SigningSchemeV4,
		Method: "PUT",
		Headers: []string{
			"Content-Type:application/octet-stream",
		},
		GoogleAccessID: conf.Email,
		PrivateKey:     conf.PrivateKey,
		Expires:        time.Now().Add(15 * time.Minute),
	}
	u, err := storage.SignedURL(bucket, object, opts)
	if err != nil {
		return "", fmt.Errorf("storage.SignedURL: %v", err)
	}
	fmt.Fprintln(w, "Generated PUT signed URL:")
	fmt.Fprintf(w, "%q\n", u)
	fmt.Fprintln(w, "You can use this URL with any user agent, for example:")
	fmt.Fprintf(w, "curl -X PUT -H 'Content-Type: application/octet-stream' --upload-file my-file %q\n", u)
	return u, nil
}

Java

import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.HttpMethod;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageException;
import com.google.cloud.storage.StorageOptions;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class GenerateV4PutObjectSignedUrl {
  /**
   * Signing a URL requires Credentials which implement ServiceAccountSigner. These can be set
   * explicitly using the Storage.SignUrlOption.signWith(ServiceAccountSigner) option. If you don't,
   * you could also pass a service account signer to StorageOptions, i.e.
   * StorageOptions().newBuilder().setCredentials(ServiceAccountSignerCredentials). In this example,
   * neither of these options are used, which means the following code only works when the
   * credentials are defined via the environment variable GOOGLE_APPLICATION_CREDENTIALS, and those
   * credentials are authorized to sign a URL. See the documentation for Storage.signUrl for more
   * details.
   */
  public static void generateV4GPutObjectSignedUrl(
      String projectId, String bucketName, String objectName) throws StorageException {
    // String projectId = "my-project-id";
    // String bucketName = "my-bucket";
    // String objectName = "my-object";

    Storage storage = StorageOptions.newBuilder().setProjectId(projectId).build().getService();

    // Define Resource
    BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(bucketName, objectName)).build();

    // Generate Signed URL
    Map<String, String> extensionHeaders = new HashMap<>();
    extensionHeaders.put("Content-Type", "application/octet-stream");

    URL url =
        storage.signUrl(
            blobInfo,
            15,
            TimeUnit.MINUTES,
            Storage.SignUrlOption.httpMethod(HttpMethod.PUT),
            Storage.SignUrlOption.withExtHeaders(extensionHeaders),
            Storage.SignUrlOption.withV4Signature());

    System.out.println("Generated PUT signed URL:");
    System.out.println(url);
    System.out.println("You can use this URL with any user agent, for example:");
    System.out.println(
        "curl -X PUT -H 'Content-Type: application/octet-stream' --upload-file my-file '"
            + url
            + "'");
  }
}

C#


using Google.Cloud.Storage.V1;
using System;
using System.Collections.Generic;
using System.Net.Http;

public class GenerateV4UploadSignedUrlSample
{
    public string GenerateV4UploadSignedUrl(
        string bucketName = "your-unique-bucket-name",
        string objectName = "your-object-name",
        string credentialFilePath = "my-local-path/my-credential-file-name")
    {
        UrlSigner urlSigner = UrlSigner.FromServiceAccountPath(credentialFilePath);

        var contentHeaders = new Dictionary<string, IEnumerable<string>>
        {
            { "Content-Type", new[] { "text/plain" } }
        };

        // V4 is the default signing version.
        UrlSigner.Options options = UrlSigner.Options.FromDuration(TimeSpan.FromHours(1));

        UrlSigner.RequestTemplate template = UrlSigner.RequestTemplate
            .FromBucket(bucketName)
            .WithObjectName(objectName)
            .WithHttpMethod(HttpMethod.Put)
            .WithContentHeaders(contentHeaders);

        string url = urlSigner.Sign(template, options);
        Console.WriteLine("Generated PUT signed URL:");
        Console.WriteLine(url);
        Console.WriteLine("You can use this URL with any user agent, for example:");
        Console.WriteLine($"curl -X PUT -H 'Content-Type: text/plain' --upload-file my-file '{url}'");
        return url;
    }
}

当客户端将文件上传到签名网址时,如果您想对上传的文件采取进一步的操作,可以通过这项变更触发第二个函数。 如需详细了解如何在更改 Cloud Storage 存储分区时触发 Cloud Functions 函数,请参阅 Cloud Storage 教程

后续步骤