对服务到服务进行身份验证

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

如果您的架构使用多项服务,则这些服务可能需要使用异步或同步方式相互通信。其中许多服务可能是私有的,并且需要凭据才能访问。

对于异步通信,您可以使用以下 Google Cloud 服务:

  • Cloud Tasks(用于一对一异步通信)
  • Pub/Sub(用于一对多、一对一和多对一异步通信)
  • Cloud Scheduler(用于定期安排的异步通信)
  • Eventarc(用于基于事件的通信)

在所有这些情况下,使用的服务都会根据您设置的配置管理与接收服务的交互。

但对于同步通信,您的服务会使用其端点网址通过 HTTP 直接调用其他服务。对于此使用场景,您应确保每项服务只能向特定服务发出请求。例如,如果您拥有 login 服务,则该服务应该能够访问 user-profiles 服务,但不能访问 search 服务。

在这种情况下,Google 建议您使用基于每项服务的用户管理服务帐号IAM 和服务身份,该服务帐号已被授予执行其工作所需的一组最低权限

此外,该请求还必须提供发起调用的服务的身份证明。为此,请配置您的发起调用的服务,以将 Google 签名的 OpenID Connect ID 令牌添加到请求中。

设置服务帐号

如需设置服务帐号,请将接收服务配置为接受来自调用服务的请求,方法是将调用服务的服务帐号设为接收服务的主帐号。然后,向该服务帐号授予 Cloud Run Invoker (roles/run.invoker) 角色。如需执行这两项任务,请按照相应标签页中的说明进行操作:

控制台界面

  1. 前往 Google Cloud 控制台:

    前往 Google Cloud 控制台

  2. 选择接收方服务。

  3. 点击右上角的显示信息面板,以显示权限标签页。

  4. 点击添加主帐号

    1. 输入调用服务的身份。默认情况下,通常是一个电子邮件地址 (PROJECT_NUMBER-compute@developer.gserviceaccount.com)。

    2. 选择角色下拉菜单中选择 Cloud Run Invoker 角色。

    3. 点击保存

gcloud

使用 gcloud run services add-iam-policy-binding 命令:

gcloud run services add-iam-policy-binding RECEIVING_SERVICE \
  --member='serviceAccount:CALLING_SERVICE_IDENTITY' \
  --role='roles/run.invoker'

其中,RECEIVING_SERVICE 是接收服务的名称,CALLING_SERVICE_IDENTITY 是服务帐号的电子邮件地址,默认情况下为 PROJECT_NUMBER-compute@developer.gserviceaccount.com

Terraform

以下 Terraform 代码会创建要公开的初始 Cloud Run 服务。

resource "google_cloud_run_service" "public" {
  name     = "public-service"
  location = "us-central1"

  template {
    spec {
      containers {
        # TODO<developer>: replace this with a public service container
        # (This service can be invoked by anyone on the internet)
        image = "us-docker.pkg.dev/cloudrun/container/hello"

        # Include a reference to the private Cloud Run
        # service's URL as an environment variable.
        env {
          name  = "URL"
          value = google_cloud_run_service.private.status[0].url
        }
      }

      # Give the "public" Cloud Run service
      # a service account's identity
      service_account_name = google_service_account.default.email
    }
  }
}

us-docker.pkg.dev/cloudrun/container/hello 替换为对您的容器映像的引用。

以下 Terraform 代码将初始服务设为公开。

data "google_iam_policy" "public" {
  binding {
    role = "roles/run.invoker"
    members = [
      "allUsers",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "public" {
  location = google_cloud_run_service.public.location
  project  = google_cloud_run_service.public.project
  service  = google_cloud_run_service.public.name

  policy_data = data.google_iam_policy.public.policy_data
}

以下 Terraform 代码创建第二个将设为私有的 Cloud Run 服务。

resource "google_cloud_run_service" "private" {
  name     = "private-service"
  location = "us-central1"

  template {
    spec {
      containers {
        // TODO<developer>: replace this with a private service container
        // (This service should only be invocable by the public service)
        image = "us-docker.pkg.dev/cloudrun/container/hello"
      }
    }
  }
}

us-docker.pkg.dev/cloudrun/container/hello 替换为对您的容器映像的引用。

以下 Terraform 代码将第二个服务设为私有。

data "google_iam_policy" "private" {
  binding {
    role = "roles/run.invoker"
    members = [
      "serviceAccount:${google_service_account.default.email}",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "private" {
  location = google_cloud_run_service.private.location
  project  = google_cloud_run_service.private.project
  service  = google_cloud_run_service.private.name

  policy_data = data.google_iam_policy.private.policy_data
}

以下 Terraform 代码会创建一个服务帐号。

resource "google_service_account" "default" {
  account_id   = "cloud-run-interservice-id"
  description  = "Identity used by a public Cloud Run service to call private Cloud Run services."
  display_name = "cloud-run-interservice-id"
}

以下 Terraform 代码允许附加到服务帐号的服务调用初始私有 Cloud Run 服务。

data "google_iam_policy" "private" {
  binding {
    role = "roles/run.invoker"
    members = [
      "serviceAccount:${google_service_account.default.email}",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "private" {
  location = google_cloud_run_service.private.location
  project  = google_cloud_run_service.private.project
  service  = google_cloud_run_service.private.name

  policy_data = data.google_iam_policy.private.policy_data
}

获取并配置 ID 令牌

为调用服务帐号授予适当的角色后,您需要执行以下操作:

  1. 提取目标对象声明 (aud) 设置为接收服务网址的 Google 签名的 ID 令牌。aud 值应保留为服务的网址,即使向特定流量标记发出请求也是如此。

  2. 将该 ID 令牌添加到接收服务请求的 Authorization: Bearer ID_TOKEN 标头中。

管理此过程最简单、最可靠的方式是使用如下所示的身份验证库生成并使用此令牌。此代码可在库可以获取身份验证凭据的任何环境中工作(甚至在 Google Cloud 外部),包括支持本地应用默认凭据的环境。

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// Example: https://my-cloud-run-service.run.app/books/delete/12345
// const url = 'https://TARGET_HOSTNAME/TARGET_URL';

// Example (Cloud Run): https://my-cloud-run-service.run.app/
// const targetAudience = 'https://TARGET_AUDIENCE/';

const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();

async function request() {
  console.info(`request ${url} with target audience ${targetAudience}`);
  const client = await auth.getIdTokenClient(targetAudience);
  const res = await client.request({url});
  console.info(res.data);
}

request().catch(err => {
  console.error(err.message);
  process.exitCode = 1;
});

Python

import urllib

import google.auth.transport.requests
import google.oauth2.id_token

def make_authorized_get_request(endpoint, audience):
    """
    make_authorized_get_request makes a GET request to the specified HTTP endpoint
    by authenticating with the ID token obtained from the google-auth client library
    using the specified audience value.
    """

    # Cloud Run uses your service's hostname as the `audience` value
    # audience = 'https://my-cloud-run-service.run.app/'
    # For Cloud Run, `endpoint` is the URL (hostname + path) receiving the request
    # endpoint = 'https://my-cloud-run-service.run.app/my/awesome/url'

    req = urllib.request.Request(endpoint)

    auth_req = google.auth.transport.requests.Request()
    id_token = google.oauth2.id_token.fetch_id_token(auth_req, audience)

    req.add_header("Authorization", f"Bearer {id_token}")
    response = urllib.request.urlopen(req)

    return response.read()

Go


import (
	"context"
	"fmt"
	"io"

	"google.golang.org/api/idtoken"
)

// `makeGetRequest` makes a request to the provided `targetURL`
// with an authenticated client using audience `audience`.
func makeGetRequest(w io.Writer, targetURL string, audience string) error {
	// Example `audience` value (Cloud Run): https://my-cloud-run-service.run.app/
	// (`targetURL` and `audience` will differ for non-root URLs and GET parameters)
	ctx := context.Background()

	// client is a http.Client that automatically adds an "Authorization" header
	// to any requests made.
	client, err := idtoken.NewClient(ctx, audience)
	if err != nil {
		return fmt.Errorf("idtoken.NewClient: %v", err)
	}

	resp, err := client.Get(targetURL)
	if err != nil {
		return fmt.Errorf("client.Get: %v", err)
	}
	defer resp.Body.Close()
	if _, err := io.Copy(w, resp.Body); err != nil {
		return fmt.Errorf("io.Copy: %v", err)
	}

	return nil
}

Java

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdTokenCredentials;
import com.google.auth.oauth2.IdTokenProvider;
import java.io.IOException;

public class Authentication {

  // makeGetRequest makes a GET request to the specified Cloud Run or
  // Cloud Functions endpoint `serviceUrl` (must be a complete URL), by
  // authenticating with an ID token retrieved from Application Default
  // Credentials using the specified `audience`.
  //
  // Example `audience` value (Cloud Run): https://my-cloud-run-service.run.app/
  public static HttpResponse makeGetRequest(String serviceUrl, String audience) throws IOException {
    GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
    if (!(credentials instanceof IdTokenProvider)) {
      throw new IllegalArgumentException("Credentials are not an instance of IdTokenProvider.");
    }
    IdTokenCredentials tokenCredential =
        IdTokenCredentials.newBuilder()
            .setIdTokenProvider((IdTokenProvider) credentials)
            .setTargetAudience(audience)
            .build();

    GenericUrl genericUrl = new GenericUrl(serviceUrl);
    HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(tokenCredential);
    HttpTransport transport = new NetHttpTransport();
    HttpRequest request = transport.createRequestFactory(adapter).buildGetRequest(genericUrl);
    return request.execute();
  }
}

使用元数据服务器

如果您因某种原因而无法使用身份验证库,则可以当容器正在 Cloud Run 上运行时从 Compute 元数据服务器提取 ID 令牌。请注意,此方法在 Google Cloud 以外无法使用,包括在本地机器中。

curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=[AUDIENCE]" \
     -H "Metadata-Flavor: Google"

其中 AUDIENCE 是您要调用的服务的网址。

对于使用服务到服务身份验证技术的应用的端到端演示,请遵循教程:保护 Cloud Run 服务安全

接收经过身份验证的请求

在接收专用服务时,您可以解析授权标头以接收不记名令牌发送的信息。

Python


from google.auth import jwt

def receive_authorized_get_request(request):
    """
    receive_authorized_get_request takes the "Authorization" header from a
    request, decodes it using the google-auth client library, and returns
    back the email from the header to the caller.
    """
    auth_header = request.headers.get("Authorization")
    if auth_header:

        # split the auth type and value from the header.
        auth_type, creds = auth_header.split(" ", 1)

        if auth_type.lower() == "bearer":
            claims = jwt.decode(creds, verify=False)
            return f"Hello, {claims['email']}!\n"

        else:
            return f"Unhandled header format ({auth_type}).\n"
    return "Hello, anonymous user.\n"

从 Google Cloud 外部调用

您可以使用工作负载身份联合或使用下载的服务帐号密钥从 Google Cloud 外部调用专用服务。以下部分介绍了这两种方法。

使用工作负载身份联合

如果您的环境使用的是工作负载身份联合支持的身份提供商,那么您可以使用此方法安全地向 Cloud Run 服务进行身份验证:

  1. 按照本页面上的设置服务帐号中的说明设置您的服务帐号。

  2. 按照配置工作负载身份联合中的说明,为您的身份提供商配置工作负载身份联合。

  3. 按照授予外部身份模拟服务帐号的权限中的说明操作。

  4. 使用 REST API 获取短期有效的令牌,但不是调用 generateAccessToken 来获取访问令牌,而是调用 generateIdToken 来获取 ID 令牌。

    例如,使用 cURL:

    ID_TOKEN=$(curl -0 -X POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/SERVICE_ACCOUNT:generateIdToken \
      -H "Content-Type: text/json; charset=utf-8" \
      -H "Authorization: Bearer $STS_TOKEN" \
      -d @- <<EOF | jq -r .token
      {
          "audience": "SERVICE_URL"
      }
    EOF)
    echo $ID_TOKEN
    

    其中,SERVICE_ACCOUNT 是工作负载身份池配置来访问的服务帐号的电子邮件地址,SERVICE_URL 是您正在调用的 Cloud Run 服务的网址。此值应保留为服务的网址,即使向特定流量标记发出请求也是如此。$STS_TOKEN 是您在工作负载身份联合说明的上一步中收到的 Security Token Service 令牌。

获得 ID 令牌后,您可以将其添加到发送给接收服务的请求中的 Authorization: Bearer ID_TOKEN 标头。

使用下载的服务帐号密钥

如果工作负载身份联合不适合您的环境,您可以使用下载的服务帐号密钥进行身份验证。更新客户端代码以将上文所述的身份验证库与应用默认凭据结合使用。

您可以使用自签名 JWT 来获取由 Google 签名的 ID 令牌,但这可能非常复杂且很容易出错。基本步骤如下:

  1. 对服务帐号 JWT 进行自签名,并将 target_audience 声明设置为接收服务的网址。target_audience 值应保留为服务的网址,即使向特定流量标记发出请求也是如此。

  2. 将自签名 JWT 替换成带有 Google 签名的 ID 令牌,该令牌的 aud 声明应设置为上述网址。

  3. 将该 ID 令牌添加到服务请求的 Authorization: Bearer ID_TOKEN 标头中。

您可以查看此 Cloud Functions 示例,获取上述步骤的示例。