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

本页面介绍如何使用凭证访问边界来创建具有缩小的 Cloud Storage 权限范围的 OAuth 2.0 访问令牌。

创建具有缩小的权限范围的令牌的过程包括以下步骤:

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

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

准备工作

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

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

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

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

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

  • Enable the IAM and Security Token Service APIs.

    Enable the APIs

授予 IAM 角色

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

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

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

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

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

定义凭证访问边界

凭据访问边界是包含一系列访问边界规则的对象。这些规则由参数组成,用于指定用户或服务账号可用的权限的上限。如需定义凭证访问边界,请创建一个列出访问边界规则及其参数的 JSON 对象。

以下是一个凭证访问边界的示例:

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:ROLE_ID"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/BUCKET_NAME"
         "availabilityCondition": {
            "expression" : "CONDITION"
      }
    ]
  }
}

替换以下内容:

  • ROLE_ID:预定义角色或自定义角色的 ID,相应角色用于定义可用资源权限的上限。例如 roles/storage.objectViewer。如需指定多个角色,请添加一个以 inRole: 为前缀后跟角色 ID 的新行。只能使用指定角色具有的权限。
  • BUCKET_NAME:相应规则所适用的 Cloud Storage 存储桶的名称。
  • CONDITION:可选。一个条件表达式,用于指定可以使用权限的 Cloud Storage 对象。例如,如果应用以下条件,则名称以 customer-a 开头的对象可以使用权限:

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

如需详细了解如何创建和自定义凭证访问边界,请参阅凭证访问边界的组成部分

如需查看凭证访问边界的潜在应用场景示例,请参阅凭证访问边界的示例

创建 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 来手动交换令牌。

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

    令牌使用方可以使用受支持的身份验证库在访问令牌到期之前自动刷新访问令牌。或者,令牌使用方可以手动刷新令牌;也可以让令牌过期,而不进行刷新。

您可以通过以下两种方式将访问令牌交换为具有缩小权限范围的令牌:

  • 客户端令牌交换(推荐):客户端从 Security Token Service API 服务器获取加密材料。借助加密材料,客户端可以在客户端上独立生成具有不同凭证访问边界规则的缩小权限范围的令牌,有效期为既定的一段时长(例如 1 小时)。这种方法可缩短延迟时间并提高效率,尤其适用于需要频繁更新凭证访问边界规则的客户端。对于需要生成多个具有缩小权限范围的唯一令牌的应用,这种方法更高效。我们建议您采用这种方法,因为它可提供更优异的性能、可扩缩性以及与未来功能的兼容性。

  • 服务器端令牌交换:每当凭证访问边界规则发生更改时,客户端都会向 Security Token Service API 服务器请求具有缩小权限范围的新令牌。此方法简单明了,但每次请求具有缩小权限范围的令牌都需要往返 Security Token Service API 服务器。由于每次请求具有缩小权限范围的令牌都需要往返 Security Token Service API,因此建议仅在客户需要使用的客户端库不支持客户端令牌交换时才使用此方法。

客户端令牌交换

如果您使用以下编程语言创建令牌代理和令牌使用方,则可以使用 Google 的身份验证库通过客户端方法自动交换和刷新令牌。

Java

对于 Java,您可以使用 com.google.auth:google-auth-library-cab-token-generator 制品 1.32.1 版或更高版本自动交换和刷新令牌。

如需检查制品版本,请在应用目录中运行以下 Maven 命令:

mvn dependency:list -DincludeArtifactIds=google-auth-library-cab-token-generator

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

import com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.CredentialAccessBoundary;
import com.google.auth.oauth2.GoogleCredentials;
import dev.cel.common.CelValidationException;
import java.io.IOException;
import java.security.GeneralSecurityException;
  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 an instance of ClientSideCredentialAccessBoundaryFactory.
    ClientSideCredentialAccessBoundaryFactory factory =
        ClientSideCredentialAccessBoundaryFactory.newBuilder()
            .setSourceCredential(sourceCredentials)
            .build();

    // Generate the token and pass it to the Token Consumer.
    try {
      return factory.generateToken(credentialAccessBoundary);
    } catch (GeneralSecurityException | CelValidationException e) {
      throw new IOException("Error generating downscoped token", e);
    }
  }

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

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.OAuth2CredentialsWithRefresh;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import java.io.IOException;
  public static String retrieveBlobWithDownscopedToken(
      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 DownscopedAccessTokenGenerator
                .getTokenFromBroker(bucketName, objectName);
          }
        };

    AccessToken downscopedToken = handler.refreshAccessToken();

    OAuth2CredentialsWithRefresh credentials =
        OAuth2CredentialsWithRefresh.newBuilder()
            .setAccessToken(downscopedToken)
            .setRefreshHandler(handler)
            .build();

    StorageOptions options = StorageOptions.newBuilder().setCredentials(credentials).build();
    Storage storage = options.getService();

    Blob blob = storage.get(bucketName, objectName);
    if (blob == null) {
      return null;
    }
    return new String(blob.getContent());
  }

服务器端令牌交换

本部分介绍了以下方法,可用于通过服务端方法交换令牌:

使用服务器端方法自动交换和刷新访问令牌

如果您使用以下某种编程语言创建令牌代理和令牌使用方,则可以使用 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
}

当令牌使用方请求具有缩小权限范围的令牌时,令牌代理的响应中会同时包含相应具有缩小权限范围的令牌以及该令牌距离到期的时间(秒数)。如果令牌已到期,服务器会拒绝该请求。如需刷新具有缩小权限范围的令牌,使用方可以在现有令牌到期之前从代理请求具有缩小权限范围的令牌。

后续步骤