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

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

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

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

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

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

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

设置服务帐号

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

控制台界面

  1. 转到 Google Cloud Console:

    转到 Google Cloud Console

  2. 选择接收方服务。

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

  4. 添加成员字段中,输入调用方服务的身份。 默认情况下,通常是一个电子邮件地址 (PROJECT_NUMBER-compute@developer.gserviceaccount.com)。

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

  6. 点击添加

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

获取并配置 ID 令牌

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

  1. 提取目标对象声明 (aud) 设置为接收服务网址的 Google 签名的 ID 令牌。

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

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

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// const url = 'https://TARGET_URL';
// let targetAudience = null;
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();

async function request() {
  if (!targetAudience) {
    // Use the request URL hostname as the target audience for requests.
    const {URL} = require('url');
    targetAudience = new URL(url);
  }
  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(service_url):
    """
    make_authorized_get_request makes a GET request to the specified HTTP endpoint
    in service_url (must be a complete URL) by authenticating with the
    ID token obtained from the google-auth client library.
    """

    req = urllib.request.Request(service_url)

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

    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.
func makeGetRequest(w io.Writer, targetURL string) error {
	// functionURL := "https://TARGET_URL"
	ctx := context.Background()

	// client is a http.Client that automatically adds an "Authorization" header
	// to any requests made.
	client, err := idtoken.NewClient(ctx, targetURL)
	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.
  public static HttpResponse makeGetRequest(String serviceUrl) 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(serviceUrl)
            .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 服务安全

从 GCP 外部调用服务

从 Google Cloud 外部调用专用服务的最佳方法是使用如上所述的身份验证库,并设置应用默认凭据

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

  1. 对服务帐号 JWT 进行自签名,并将 target_audience 声明设置为接收服务的网址。

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

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

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