限制凭据的 Cloud Storage 权限

本页面介绍如何使用凭据访问边界来缩小或限制短期有效的凭据可以使用的 Identity and Access Management (IAM) 权限范围。

您可以使用凭据访问边界生成 OAuth 2.0 访问令牌,这些令牌代表服务账号,但具有的权限少于服务账号。例如,如果您的某个客户需要访问您控制的 Cloud Storage 数据,您可以执行以下操作:

  1. 创建一个可访问您拥有的每个 Cloud Storage 存储桶的服务账号。
  2. 为此服务账号生成 OAuth 2.0 访问令牌。
  3. 应用凭据访问边界,以仅允许访问包含您的客户数据的存储桶。

凭据访问边界的工作原理

要缩小权限范围,您需要定义凭据访问边界(用来指定短期有效的凭据可以访问的资源),以及在各资源上可用权限的上限。然后,您可以创建一个短期有效的凭据,并用其交换遵循凭据访问边界的新凭据。

如果您需要针对每个会话为主账号提供一组不同的权限,则与创建多个不同的服务账号并为每个服务账号授予一组不同的角色相比,使用凭据访问边界更高效。

凭据访问边界的示例

以下各个部分展示了常见使用场景的凭据访问边界示例。您在使用 OAuth 2.0 访问令牌换取作用域令牌时,可以使用凭据访问边界。

限制存储桶的权限

以下示例展示了一个简单的凭据访问边界。它应用于 Cloud Storage 存储桶 example-bucket,并且将权限上限设置为 Storage Object Viewer 角色 (roles/storage.objectViewer) 中包含的权限:

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket"
      }
    ]
  }
}

限制多个存储桶的权限

以下示例展示了一个凭据访问边界,其中包含多个存储桶的规则:

  • Cloud Storage 存储桶 example-bucket-1:对于此存储桶,只有 Storage Object Viewer 角色 (roles/storage.objectViewer) 中的权限可用。
  • Cloud Storage 存储桶 example-bucket-2:对于此存储桶,只有 Storage Object Creator 角色 (roles/storage.objectCreator) 中的权限可用。
{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket-1"
      },
      {
        "availablePermissions": [
          "inRole:roles/storage.objectCreator"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket-2"
      }
    ]
  }
}

限制特定对象的权限

您还可以使用 IAM Conditions 指定主账号可以访问哪些 Cloud Storage 对象。例如,您可以添加一个条件,该条件使权限可用于名称以 customer-a 开头的对象:

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket",
        "availabilityCondition": {
          "expression" : "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"
        }
      }
    ]
  }
}

列出对象时限制权限

当您列出 Cloud Storage 存储桶中的对象时,会针对存储桶资源(而不是对象资源)调用方法。因此,如果系统为列出请求计算了条件,并且该条件引用了资源名称,则资源名称将标识存储桶,而不是该存储桶中的对象。例如,当您列出 example-bucket 中的对象时,资源名称为 projects/_/buckets/example-bucket

当您列出对象时,此命名惯例可能会导致意外行为。例如,假设您需要一个凭据访问边界,用于允许查看 example-bucket 中前缀为 customer-a/invoices/ 的对象。您可以尝试在凭据访问边界中使用以下条件:

不完整:仅检查资源名称的条件

resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a/invoices/')

此条件适用于读取对象,但不适用于列出对象:

  • 当主账号尝试读取 example-bucket 中前缀为 customer-a/invoices/ 的对象时,该条件的计算结果为 true
  • 当主账号尝试列出具有该前缀的对象时,该条件的计算结果为 falseresource.name 的值为 projects/_/buckets/example-bucket(没有以 projects/_/buckets/example-bucket/objects/customer-a/invoices/ 开头)。

为了防止此问题,除了使用 resource.name.startsWith() 之外,您的条件还可以检查名为 storage.googleapis.com/objectListPrefixAPI 特性。此特性包含用于过滤对象列表的 prefix 参数的值。因此,您可以编写一个引用 prefix 参数的值的条件。

以下示例展示了如何在条件中使用 API 特性。它允许读取和列出 example-bucket 中前缀为 customer-a/invoices/ 的对象:

完整:检查资源名称和前缀的条件

resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a/invoices/')  ||
    api.getAttribute('storage.googleapis.com/objectListPrefix', '')
                     .startsWith('customer-a/invoices/')

现在,您可以在凭据访问边界中使用此条件:

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket",
        "availabilityCondition": {
          "expression":
            "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a/invoices/') || api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('customer-a/invoices/')"
        }
      }
    ]
  }
}

准备工作

在使用凭据访问边界之前,请确保满足以下要求:

  • 您需要只针对 Cloud Storage 缩小权限范围,而不对其他 Google Cloud 服务这样做。

    如果您需要缩小针对其他 Google Cloud 服务的权限范围,您可以创建多个服务账号并为每个服务账号授予一组不同的角色。

  • 您可以使用 OAuth 2.0 访问令牌进行身份验证。 其他类型的短期有效的凭据不支持凭据访问边界功能。

此外,您还必须启用所需的 API:

  • Enable the IAM and Security Token Service APIs.

    Enable the APIs

创建具有缩小的权限范围的短期有效的凭据

要创建具有缩小的权限范围的 OAuth 2.0 访问令牌,请按以下步骤操作:

  1. 向用户或服务账号授予相应的 IAM 角色
  2. 定义一个凭据访问边界,以设置用户或服务账号可用权限的上限。
  3. 为用户或服务账号创建 OAuth 2.0 访问令牌
  4. OAuth 2.0 访问令牌交换遵循凭据访问边界的新令牌。

然后,您可以使用具有缩小的权限范围的新 OAuth 2.0 访问令牌,验证针对 Cloud Storage 的请求的身份。

授予 IAM 角色

凭据访问边界设置了可用于特定资源的权限的上限。它可以删减主账号已获取的权限,但无法添加该主账号尚未拥有的权限。

因此,您还必须在 Cloud Storage 存储桶上,或更高级别的资源(例如项目)上向主账号授予提供所需权限的角色。

例如,假设您需要创建一个具有缩小的权限范围的短期有效的凭据,以允许服务账号在存储桶中创建对象:

  • 您必须至少向服务账号授予包含 storage.objects.create 权限的角色,例如 Storage Object Creator 角色 (roles/storage.objectCreator)。凭据访问边界也必须包含此权限。
  • 您还可以授予其包含更多权限的角色,例如 Storage Object Admin 角色 (roles/storage.objectAdmin)。服务账号只能使用同时出现在角色授权和凭据访问边界中的权限。

如需了解 Cloud Storage 的预定义角色,请参阅 Cloud Storage 角色

凭据访问边界的组成部分

凭据访问边界是包含一系列访问边界规则的对象。每条规则都包含以下信息:

  • 应用该规则的资源。
  • 在该资源上可用的权限上限。
  • 可选:进一步限制权限的条件。条件包括以下内容:
    • 计算结果为 truefalse 的条件表达式。如果计算结果为 true,则允许访问;否则拒绝访问。
    • 可选:标识条件的标题。
    • 可选:包含有关条件的更多信息的说明。

如果您将凭据访问边界应用于短期有效的凭据,则该凭据只能访问凭据访问边界中的资源,不能获得在其他资源上可用的权限。

凭证访问边界最多可以包含 10 条访问边界规则。您只能为每个短期有效的凭据应用一个凭据访问边界。

以 JSON 对象的形式表示时,凭据访问边界包含以下字段:

字段
accessBoundary

object

凭据访问边界的封装容器。

accessBoundary.accessBoundaryRules[]

object

应用于短期有效的凭据的访问边界规则的列表。

accessBoundary.accessBoundaryRules[].availablePermissions[]

string

用于定义对于资源可用的权限上限的列表。

每个值都是一个 IAM 预定义角色自定义角色的标识符,并且带有前缀 inRole:。例如:inRole:roles/storage.objectViewer。只能使用这些角色中的权限。

accessBoundary.accessBoundaryRules[].availableResource

string

应用规则的 Cloud Storage 存储桶的完整资源名称。请使用此格式://storage.googleapis.com/projects/_/buckets/bucket-name

accessBoundary.accessBoundaryRules[].availabilityCondition

object

可选。限制对特定 Cloud Storage 对象的权限可用性的条件。

如果要使权限可用于特定的对象(而不是 Cloud Storage 存储桶中的所有对象),请使用此字段。

accessBoundary.accessBoundaryRules[].availabilityCondition.expression

string

一个条件表达式,用于指定可以使用权限的 Cloud Storage 对象。

如需了解如何在条件表达式中引用特定的对象,请参阅 resource.name 特性

accessBoundary.accessBoundaryRules[].availabilityCondition.title

string

可选。标识条件目的的短字符串。

accessBoundary.accessBoundaryRules[].availabilityCondition.description

string

可选。有关条件目的的详细信息。

如需查看 JSON 格式的示例,请参阅本页面上的凭据访问边界示例

创建 OAuth 2.0 访问令牌

要创建具有缩小的权限范围的短期有效的凭据,您必须先创建一个常规的 OAuth 2.0 访问令牌。然后将常规凭据交换为具有缩小的权限范围的凭据。在创建访问令牌时,请使用 OAuth 2.0 范围 https://www.googleapis.com/auth/cloud-platform

要为服务账号创建访问令牌,您可以完成服务器到服务器的 OAuth 2.0 流程,或者可以使用 Service Account Credentials API 生成 OAuth 2.0 访问令牌

如需为用户创建访问令牌,请参阅获取 OAuth 2.0 访问令牌。您还可以使用 OAuth 2.0 Playground,为您自己的 Google 账号创建访问令牌。

交换 OAuth 2.0 访问令牌

创建 OAuth 2.0 访问令牌后,您可以将访问令牌交换为符合凭据访问边界的缩小的权限范围的令牌。此过程通常涉及令牌代理和令牌使用方

  • 令牌代理负责定义凭据访问边界以及将访问令牌交换为具有缩小的权限范围的令牌。

    令牌代理可以使用受支持的身份验证库自动交换访问令牌,也可以调用 Security Token Service 来手动交换令牌

  • 令牌使用方会从令牌代理请求具有缩小的权限范围的访问令牌,然后使用具有缩小的权限范围的访问令牌来执行其他操作。

    令牌使用方可以使用受支持的身份验证库在访问令牌到期之前自动刷新访问令牌。或者,令牌使用方可以手动刷新令牌,也可以允许令牌过期,而无需刷新。

自动交换和刷新访问令牌

如果您使用以下某种语言创建令牌代理和令牌使用方,则可以使用 Google 的身份验证库自动交换和刷新令牌:

Go

对于 Go,您可以使用 golang.org/x/oauth2 软件包 v0.0.0-20210819190943-2bc19b11175f 版或更高版本自动交换和刷新令牌。

如需检查正在使用的软件包版本,请在应用目录中运行以下命令:

go list -m golang.org/x/oauth2

以下示例展示了令牌代理如何生成具有缩小的权限范围的令牌:


import (
	"context"
	"fmt"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"golang.org/x/oauth2/google/downscope"
)

// createDownscopedToken would be run on the token broker in order to generate
// a downscoped access token that only grants access to objects whose name begins with prefix.
// The token broker would then pass the newly created token to the requesting token consumer for use.
func createDownscopedToken(bucketName string, prefix string) error {
	// bucketName := "foo"
	// prefix := "profile-picture-"

	ctx := context.Background()
	// A condition can optionally be provided to further restrict access permissions.
	condition := downscope.AvailabilityCondition{
		Expression:  "resource.name.startsWith('projects/_/buckets/" + bucketName + "/objects/" + prefix + "')",
		Title:       prefix + " Only",
		Description: "Restricts a token to only be able to access objects that start with `" + prefix + "`",
	}
	// Initializes an accessBoundary with one Rule which restricts the downscoped
	// token to only be able to access the bucket "bucketName" and only grants it the
	// permission "storage.objectViewer".
	accessBoundary := []downscope.AccessBoundaryRule{
		{
			AvailableResource:    "//storage.googleapis.com/projects/_/buckets/" + bucketName,
			AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
			Condition:            &condition, // Optional
		},
	}

	// This Source can be initialized in multiple ways; the following example uses
	// Application Default Credentials.
	var rootSource oauth2.TokenSource

	// You must provide the "https://www.googleapis.com/auth/cloud-platform" scope.
	rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform")
	if err != nil {
		return fmt.Errorf("failed to generate rootSource: %w", err)
	}

	// downscope.NewTokenSource constructs the token source with the configuration provided.
	dts, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary})
	if err != nil {
		return fmt.Errorf("failed to generate downscoped token source: %w", err)
	}
	// Token() uses the previously declared TokenSource to generate a downscoped token.
	tok, err := dts.Token()
	if err != nil {
		return fmt.Errorf("failed to generate token: %w", err)
	}
	// Pass this token back to the token consumer.
	_ = tok
	return nil
}

以下示例展示了令牌使用方如何使用刷新处理程序自动获取和刷新具有缩小的权限范围的令牌:


import (
	"context"
	"fmt"
	"io"

	"golang.org/x/oauth2/google"
	"golang.org/x/oauth2/google/downscope"

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

// A token consumer should define their own tokenSource. In the Token() method,
// it should send a query to a token broker requesting a downscoped token.
// The token broker holds the root credential that is used to generate the
// downscoped token.
type localTokenSource struct {
	ctx        context.Context
	bucketName string
	brokerURL  string
}

func (lts localTokenSource) Token() (*oauth2.Token, error) {
	var remoteToken *oauth2.Token
	// Usually you would now retrieve remoteToken, an oauth2.Token, from token broker.
	// This snippet performs the same functionality locally.
	accessBoundary := []downscope.AccessBoundaryRule{
		{
			AvailableResource:    "//storage.googleapis.com/projects/_/buckets/" + lts.bucketName,
			AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
		},
	}
	rootSource, err := google.DefaultTokenSource(lts.ctx, "https://www.googleapis.com/auth/cloud-platform")
	if err != nil {
		return nil, fmt.Errorf("failed to generate rootSource: %w", err)
	}
	dts, err := downscope.NewTokenSource(lts.ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary})
	if err != nil {
		return nil, fmt.Errorf("failed to generate downscoped token source: %w", err)
	}
	// Token() uses the previously declared TokenSource to generate a downscoped token.
	remoteToken, err = dts.Token()
	if err != nil {
		return nil, fmt.Errorf("failed to generate token: %w", err)
	}

	return remoteToken, nil
}

// getObjectContents will read the contents of an object in Google Storage
// named objectName, contained in the bucket "bucketName".
func getObjectContents(output io.Writer, bucketName string, objectName string) error {
	// bucketName := "foo"
	// prefix := "profile-picture-"

	ctx := context.Background()

	thisTokenSource := localTokenSource{
		ctx:        ctx,
		bucketName: bucketName,
		brokerURL:  "yourURL.com/internal/broker",
	}

	// Wrap the TokenSource in an oauth2.ReuseTokenSource to enable automatic refreshing.
	refreshableTS := oauth2.ReuseTokenSource(nil, thisTokenSource)
	// You can now use the token source to access Google Cloud Storage resources as follows.
	storageClient, err := storage.NewClient(ctx, option.WithTokenSource(refreshableTS))
	if err != nil {
		return fmt.Errorf("failed to create the storage client: %w", err)
	}
	defer storageClient.Close()
	bkt := storageClient.Bucket(bucketName)
	obj := bkt.Object(objectName)
	rc, err := obj.NewReader(ctx)
	if err != nil {
		return fmt.Errorf("failed to retrieve the object: %w", err)
	}
	defer rc.Close()
	data, err := io.ReadAll(rc)
	if err != nil {
		return fmt.Errorf("could not read the object's contents: %w", err)
	}
	// Data now contains the contents of the requested object.
	output.Write(data)
	return nil
}

Java

对于 Java,您可以使用 com.google.auth:google-auth-library-oauth2-http 工件 1.1.0 版或更高版本自动交换和刷新令牌。

如需检查您正在使用的工件的版本,请在应用目录中运行以下 Maven 命令:

mvn dependency:list -DincludeArtifactIds=google-auth-library-oauth2-http

以下示例展示了令牌代理如何生成具有缩小的权限范围的令牌:

public static AccessToken getTokenFromBroker(String bucketName, String objectPrefix)
    throws IOException {
  // Retrieve the source credentials from ADC.
  GoogleCredentials sourceCredentials =
      GoogleCredentials.getApplicationDefault()
          .createScoped("https://www.googleapis.com/auth/cloud-platform");

  // Initialize the Credential Access Boundary rules.
  String availableResource = "//storage.googleapis.com/projects/_/buckets/" + bucketName;

  // Downscoped credentials will have readonly access to the resource.
  String availablePermission = "inRole:roles/storage.objectViewer";

  // Only objects starting with the specified prefix string in the object name will be allowed
  // read access.
  String expression =
      "resource.name.startsWith('projects/_/buckets/"
          + bucketName
          + "/objects/"
          + objectPrefix
          + "')";

  // Build the AvailabilityCondition.
  CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition availabilityCondition =
      CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
          .setExpression(expression)
          .build();

  // Define the single access boundary rule using the above properties.
  CredentialAccessBoundary.AccessBoundaryRule rule =
      CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
          .setAvailableResource(availableResource)
          .addAvailablePermission(availablePermission)
          .setAvailabilityCondition(availabilityCondition)
          .build();

  // Define the Credential Access Boundary with all the relevant rules.
  CredentialAccessBoundary credentialAccessBoundary =
      CredentialAccessBoundary.newBuilder().addRule(rule).build();

  // Create the downscoped credentials.
  DownscopedCredentials downscopedCredentials =
      DownscopedCredentials.newBuilder()
          .setSourceCredential(sourceCredentials)
          .setCredentialAccessBoundary(credentialAccessBoundary)
          .build();

  // Retrieve the token.
  // This will need to be passed to the Token Consumer.
  AccessToken accessToken = downscopedCredentials.refreshAccessToken();
  return accessToken;
}

以下示例展示了令牌使用方如何使用刷新处理程序自动获取和刷新具有缩小的权限范围的令牌:

public static void tokenConsumer(final String bucketName, final String objectName)
    throws IOException {
  // You can pass an `OAuth2RefreshHandler` to `OAuth2CredentialsWithRefresh` which will allow the
  // library to seamlessly handle downscoped token refreshes on expiration.
  OAuth2CredentialsWithRefresh.OAuth2RefreshHandler handler =
      new OAuth2CredentialsWithRefresh.OAuth2RefreshHandler() {
        @Override
        public AccessToken refreshAccessToken() throws IOException {
          // The common pattern of usage is to have a token broker pass the downscoped short-lived
          // access tokens to a token consumer via some secure authenticated channel.
          // For illustration purposes, we are generating the downscoped token locally.
          // We want to test the ability to limit access to objects with a certain prefix string
          // in the resource bucket. objectName.substring(0, 3) is the prefix here. This field is
          // not required if access to all bucket resources are allowed. If access to limited
          // resources in the bucket is needed, this mechanism can be used.
          return getTokenFromBroker(bucketName, objectName.substring(0, 3));
        }
      };

  // Downscoped token retrieved from token broker.
  AccessToken downscopedToken = handler.refreshAccessToken();

  // Create the OAuth2CredentialsWithRefresh from the downscoped token and pass a refresh handler
  // which will handle token expiration.
  // This will allow the consumer to seamlessly obtain new downscoped tokens on demand every time
  // token expires.
  OAuth2CredentialsWithRefresh credentials =
      OAuth2CredentialsWithRefresh.newBuilder()
          .setAccessToken(downscopedToken)
          .setRefreshHandler(handler)
          .build();

  // Use the credentials with the Cloud Storage SDK.
  StorageOptions options = StorageOptions.newBuilder().setCredentials(credentials).build();
  Storage storage = options.getService();

  // Call Cloud Storage APIs.
  Blob blob = storage.get(bucketName, objectName);
  String content = new String(blob.getContent());
  System.out.println(
      "Retrieved object, "
          + objectName
          + ", from bucket,"
          + bucketName
          + ", with content: "
          + content);
}

Node.js

对于 Node.js,您可以使用 google-auth-library 软件包 7.9.0 版或更高版本自动交换和刷新令牌。

如需检查正在使用的软件包版本,请在应用目录中运行以下命令:

npm list google-auth-library

以下示例展示了令牌代理如何生成具有缩小的权限范围的令牌:

// Imports the Google Auth libraries.
const {GoogleAuth, DownscopedClient} = require('google-auth-library');
/**
 * Simulates token broker generating downscoped tokens for specified bucket.
 *
 * @param bucketName The name of the Cloud Storage bucket.
 * @param objectPrefix The prefix string of the object name. This is used
 *        to ensure access is restricted to only objects starting with this
 *        prefix string.
 */
async function getTokenFromBroker(bucketName, objectPrefix) {
  const googleAuth = new GoogleAuth({
    scopes: 'https://www.googleapis.com/auth/cloud-platform',
  });

  // Define the Credential Access Boundary object.
  const cab = {
    // Define the access boundary.
    accessBoundary: {
      // Define the single access boundary rule.
      accessBoundaryRules: [
        {
          availableResource: `//storage.googleapis.com/projects/_/buckets/${bucketName}`,
          // Downscoped credentials will have readonly access to the resource.
          availablePermissions: ['inRole:roles/storage.objectViewer'],
          // Only objects starting with the specified prefix string in the object name
          // will be allowed read access.
          availabilityCondition: {
            expression:
              "resource.name.startsWith('projects/_/buckets/" +
              `${bucketName}/objects/${objectPrefix}')`,
          },
        },
      ],
    },
  };

  // Obtain an authenticated client via ADC.
  const client = await googleAuth.getClient();

  // Use the client to create a DownscopedClient.
  const cabClient = new DownscopedClient(client, cab);

  // Refresh the tokens.
  const refreshedAccessToken = await cabClient.getAccessToken();

  // This will need to be passed to the token consumer.
  return refreshedAccessToken;
}

以下示例展示了令牌使用方如何提供自动获取和刷新具有缩小的权限范围的令牌的刷新处理程序:

// Imports the Google Auth and Google Cloud libraries.
const {OAuth2Client} = require('google-auth-library');
const {Storage} = require('@google-cloud/storage');
/**
 * Simulates token consumer generating calling GCS APIs using generated
 * downscoped tokens for specified bucket.
 *
 * @param bucketName The name of the Cloud Storage bucket.
 * @param objectName The name of the object in the Cloud Storage bucket
 *        to read.
 */
async function tokenConsumer(bucketName, objectName) {
  // Create the OAuth credentials (the consumer).
  const oauth2Client = new OAuth2Client();
  // We are defining a refresh handler instead of a one-time access
  // token/expiry pair.
  // This will allow the consumer to obtain new downscoped tokens on
  // demand every time a token is expired, without any additional code
  // changes.
  oauth2Client.refreshHandler = async () => {
    // The common pattern of usage is to have a token broker pass the
    // downscoped short-lived access tokens to a token consumer via some
    // secure authenticated channel. For illustration purposes, we are
    // generating the downscoped token locally. We want to test the ability
    // to limit access to objects with a certain prefix string in the
    // resource bucket. objectName.substring(0, 3) is the prefix here. This
    // field is not required if access to all bucket resources are allowed.
    // If access to limited resources in the bucket is needed, this mechanism
    // can be used.
    const refreshedAccessToken = await getTokenFromBroker(
      bucketName,
      objectName.substring(0, 3)
    );
    return {
      access_token: refreshedAccessToken.token,
      expiry_date: refreshedAccessToken.expirationTime,
    };
  };

  const storageOptions = {
    projectId: process.env.GOOGLE_CLOUD_PROJECT,
    authClient: oauth2Client,
  };

  const storage = new Storage(storageOptions);
  const downloadFile = await storage
    .bucket(bucketName)
    .file(objectName)
    .download();
  console.log(downloadFile.toString('utf8'));
}

Python

对于 Python,您可以使用 google-auth 软件包 2.0.0 版或更高版本自动交换和刷新令牌。

如需检查正在使用的软件包版本,请在安装该软件包的环境中运行以下命令:

pip show google-auth

以下示例展示了令牌代理如何生成具有缩小的权限范围的令牌:

import google.auth

from google.auth import downscoped
from google.auth.transport import requests

def get_token_from_broker(bucket_name, object_prefix):
    """Simulates token broker generating downscoped tokens for specified bucket.

    Args:
        bucket_name (str): The name of the Cloud Storage bucket.
        object_prefix (str): The prefix string of the object name. This is used
            to ensure access is restricted to only objects starting with this
            prefix string.

    Returns:
        Tuple[str, datetime.datetime]: The downscoped access token and its expiry date.
    """
    # Initialize the Credential Access Boundary rules.
    available_resource = f"//storage.googleapis.com/projects/_/buckets/{bucket_name}"
    # Downscoped credentials will have readonly access to the resource.
    available_permissions = ["inRole:roles/storage.objectViewer"]
    # Only objects starting with the specified prefix string in the object name
    # will be allowed read access.
    availability_expression = (
        "resource.name.startsWith('projects/_/buckets/{}/objects/{}')".format(
            bucket_name, object_prefix
        )
    )
    availability_condition = downscoped.AvailabilityCondition(availability_expression)
    # Define the single access boundary rule using the above properties.
    rule = downscoped.AccessBoundaryRule(
        available_resource=available_resource,
        available_permissions=available_permissions,
        availability_condition=availability_condition,
    )
    # Define the Credential Access Boundary with all the relevant rules.
    credential_access_boundary = downscoped.CredentialAccessBoundary(rules=[rule])

    # Retrieve the source credentials via ADC.
    source_credentials, _ = google.auth.default()
    if source_credentials.requires_scopes:
        source_credentials = source_credentials.with_scopes(
            ["https://www.googleapis.com/auth/cloud-platform"]
        )

    # Create the downscoped credentials.
    downscoped_credentials = downscoped.Credentials(
        source_credentials=source_credentials,
        credential_access_boundary=credential_access_boundary,
    )

    # Refresh the tokens.
    downscoped_credentials.refresh(requests.Request())

    # These values will need to be passed to the token consumer.
    access_token = downscoped_credentials.token
    expiry = downscoped_credentials.expiry
    return (access_token, expiry)

以下示例展示了令牌使用方如何提供自动获取和刷新具有缩小的权限范围的令牌的刷新处理程序:

from google.cloud import storage
from google.oauth2 import credentials

def token_consumer(bucket_name, object_name):
    """Tests token consumer readonly access to the specified object.

    Args:
        bucket_name (str): The name of the Cloud Storage bucket.
        object_name (str): The name of the object in the Cloud Storage bucket
            to read.
    """

    # Create the OAuth credentials from the downscoped token and pass a
    # refresh handler to handle token expiration. We are passing a
    # refresh_handler instead of a one-time access token/expiry pair.
    # This will allow the consumer to obtain new downscoped tokens on
    # demand every time a token is expired, without any additional code
    # changes.
    def refresh_handler(request, scopes=None):
        # The common pattern of usage is to have a token broker pass the
        # downscoped short-lived access tokens to a token consumer via some
        # secure authenticated channel.
        # For illustration purposes, we are generating the downscoped token
        # locally.
        # We want to test the ability to limit access to objects with a certain
        # prefix string in the resource bucket. object_name[0:3] is the prefix
        # here. This field is not required if access to all bucket resources are
        # allowed. If access to limited resources in the bucket is needed, this
        # mechanism can be used.
        return get_token_from_broker(bucket_name, object_prefix=object_name[0:3])

    creds = credentials.Credentials(
        None,
        scopes=["https://www.googleapis.com/auth/cloud-platform"],
        refresh_handler=refresh_handler,
    )

    # Initialize a Cloud Storage client with the oauth2 credentials.
    storage_client = storage.Client(credentials=creds)
    # The token broker has readonly access to the specified bucket object.
    bucket = storage_client.bucket(bucket_name)
    blob = bucket.blob(object_name)
    print(blob.download_as_bytes().decode("utf-8"))

手动交换和刷新访问令牌

令牌代理可以使用 Security Token Service API 将访问令牌交换为具有缩小的权限范围的访问令牌。然后,令牌代理可以向令牌使用方提供具有缩小的权限范围的令牌。

要交换访问令牌,请使用以下 HTTP 方法和网址:

POST https://sts.googleapis.com/v1/token

将请求中的 Content-Type 标头设置为 application/x-www-form-urlencoded。在请求正文中添加以下字段:

字段
grant_type

string

使用 urn:ietf:params:oauth:grant-type:token-exchange 值。

options

string

JSON 格式的凭据访问边界,采用百分号编码进行编码。

requested_token_type

string

使用 urn:ietf:params:oauth:token-type:access_token 值。

subject_token

string

您想要交换的 OAuth 2.0 访问令牌。

subject_token_type

string

使用 urn:ietf:params:oauth:token-type:access_token 值。

响应是包含以下字段的 JSON 对象:

字段
access_token

string

符合凭据访问边界的具有缩小的权限范围的 OAuth 2.0 访问令牌。

expires_in

number

具有缩小的权限范围的令牌到期前的时间量(以秒为单位)。

仅当原始访问令牌代表服务账号时才会显示此字段。如果此字段不存在,则具有缩小的权限范围的令牌的到期时间与原始访问令牌的到期时间相同。

issued_token_type

string

包含 urn:ietf:params:oauth:token-type:access_token 值。

token_type

string

包含 Bearer 值。

例如,如果 JSON 格式的凭据访问边界存储在 ./access-boundary.json 文件中,您可以使用以下 curl 命令交换访问令牌。将 original-token 替换为原始访问令牌:

curl -H "Content-Type:application/x-www-form-urlencoded" \
    -X POST \
    https://sts.googleapis.com/v1/token \
    -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token_type=urn:ietf:params:oauth:token-type:access_token&requested_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=original-token" \
    --data-urlencode "options=$(cat ./access-boundary.json)"

响应类似于以下示例:

{
  "access_token": "ya29.dr.AbCDeFg-123456...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 3600
}

当令牌使用方请求具有缩小的权限范围的令牌时,令牌代理应同时以具有缩小的权限范围的令牌和令牌到期前的秒数进行响应。如需刷新具有缩小的权限范围的令牌,使用方可以在现有令牌到期之前从代理请求具有缩小的权限范围的令牌。

后续步骤