API キーで API アクセスを制限する

API キーを使用すると、特定の API メソッドや API のすべてのメソッドに対するアクセスを制限できます。このページでは、API キーを持つクライアントに対する API アクセスの制限方法と API キーの作成方法について説明します。

Extensible Service Proxy(ESP) は Service Control API を使用して、API キーと、その API キーとプロジェクトで有効にされた API との関連付けを検証します。API キーを必須とするように API が設定されている場合は、保護されたメソッド、クラス、API へのリクエストは適切なキーがない限り拒否されます。適切なキーとは、プロジェクト内で生成されたものか、API を有効にする権限を付与されたデベロッパーに属する別のプロジェクトで生成されたもののことです。API キーがどのプロジェクトで作成されたかはログに記録されず、リクエスト ヘッダーにも追加されません。ただし、特定のコンシューマ プロジェクトのフィルタリングに記載されているとおり、クライアントが関連付けられている Google Cloud プロジェクトは、[エンドポイント] の [サービス] に表示されます。

API キーの作成が必要な Google Cloud プロジェクトについては、API キーにより保護されている API を共有するをご覧ください。

gRPC サービスのデフォルトでは、すべての API メソッドでアクセス用の API キーが必要になります。API 全体または特定のメソッドの API キーを必須とする設定を無効にできます。これを行うには、次の手順で説明するように、サービス構成に usage セクションを追加してルールとセレクタを構成します。

すべての API メソッドへのアクセスを制限または許可する

API のアクセスに API キーが必要ないことを指定するには:

  1. プロジェクトの gRPC サービス構成ファイルをテキスト エディタで開き、usage セクションを探します(このセクションがない場合は追加します)。

  2. usage セクションで、次のように allow_unregistered_calls ルールを指定します。selector でワイルドカード "*" を使用すると、API のすべてのメソッドにルールが適用されます。

    usage:
      rules:
      # All methods can be called without an API Key.
      - selector: "*"
        allow_unregistered_calls: true
    

メソッドの API キー制限を削除する

API のアクセスを制限している場合に、特定のメソッドについてのみ API キー検証を無効にするには:

  1. プロジェクトの gRPC サービス構成ファイルをテキスト エディタで開き、usage セクションを探します(セクションがない場合には追加します)。

  2. usage セクションで、次のように allow_unregistered_calls ルールを指定します。selector は、指定したメソッドにのみルールが適用されることを意味します(この場合は ListShelves)。

    usage:
      rules:
      # ListShelves method can be called without an API Key.
      - selector: endpoints.examples.bookstore.Bookstore.ListShelves
        allow_unregistered_calls: true
    

API キーを使って API を呼び出す

API の呼び出し方法は、呼び出し元が gRPC クライアントか HTTP クライアントかによって異なります。

gRPC クライアント

メソッドで API キーが必要な場合、gRPC クライアントがメソッドの呼び出しでキー値を x-api-key メタデータとして渡す必要があります。

Python

def run(host, port, api_key, auth_token, timeout, use_tls, servername_override, ca_path):
    """Makes a basic ListShelves call against a gRPC Bookstore server."""

    if use_tls:
        with open(ca_path, 'rb') as f:
            creds = grpc.ssl_channel_credentials(f.read())
        channel_opts = ()
        if servername_override:
            channel_opts += ((
                        'grpc.ssl_target_name_override', servername_override,),)
        channel = grpc.secure_channel(f'{host}:{port}', creds, channel_opts)
    else:
        channel = grpc.insecure_channel(f'{host}:{port}')

    stub = bookstore_pb2_grpc.BookstoreStub(channel)
    metadata = []
    if api_key:
        metadata.append(('x-api-key', api_key))
    if auth_token:
        metadata.append(('authorization', 'Bearer ' + auth_token))
    shelves = stub.ListShelves(empty_pb2.Empty(), timeout, metadata=metadata)
    print(f'ListShelves: {shelves}')

Java

private static final class Interceptor implements ClientInterceptor {
  private final String apiKey;
  private final String authToken;

  private static Logger LOGGER = Logger.getLogger("InfoLogging");

  private static Metadata.Key<String> API_KEY_HEADER =
      Metadata.Key.of("x-api-key", Metadata.ASCII_STRING_MARSHALLER);
  private static Metadata.Key<String> AUTHORIZATION_HEADER =
      Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);

  public Interceptor(String apiKey, String authToken) {
    this.apiKey = apiKey;
    this.authToken = authToken;
  }

  @Override
  public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
      MethodDescriptor<ReqT,RespT> method, CallOptions callOptions, Channel next) {
    LOGGER.info("Intercepted " + method.getFullMethodName());
    ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);

    call = new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(call) {
      @Override
      public void start(Listener<RespT> responseListener, Metadata headers) {
        if (apiKey != null && !apiKey.isEmpty()) {
          LOGGER.info("Attaching API Key: " + apiKey);
          headers.put(API_KEY_HEADER, apiKey);
        }
        if (authToken != null && !authToken.isEmpty()) {
          System.out.println("Attaching auth token");
          headers.put(AUTHORIZATION_HEADER, "Bearer " + authToken);
        }
        super.start(responseListener, headers);
      }
    };
    return call;
  }
}

Go

func main() {
	flag.Parse()

	// Set up a connection to the server.
	conn, err := grpc.Dial(*addr, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	if *keyfile != "" {
		log.Printf("Authenticating using Google service account key in %s", *keyfile)
		keyBytes, err := ioutil.ReadFile(*keyfile)
		if err != nil {
			log.Fatalf("Unable to read service account key file %s: %v", *keyfile, err)
		}

		tokenSource, err := google.JWTAccessTokenSourceFromJSON(keyBytes, *audience)
		if err != nil {
			log.Fatalf("Error building JWT access token source: %v", err)
		}
		jwt, err := tokenSource.Token()
		if err != nil {
			log.Fatalf("Unable to generate JWT token: %v", err)
		}
		*token = jwt.AccessToken
		// NOTE: the generated JWT token has a 1h TTL.
		// Make sure to refresh the token before it expires by calling TokenSource.Token() for each outgoing requests.
		// Calls to this particular implementation of TokenSource.Token() are cheap.
	}

	ctx := context.Background()
	if *key != "" {
		log.Printf("Using API key: %s", *key)
		ctx = metadata.AppendToOutgoingContext(ctx, "x-api-key", *key)
	}
	if *token != "" {
		log.Printf("Using authentication token: %s", *token)
		ctx = metadata.AppendToOutgoingContext(ctx, "Authorization", fmt.Sprintf("Bearer %s", *token))
	}

	// Contact the server and print out its response.
	name := defaultName
	if len(flag.Args()) > 0 {
		name = flag.Arg(0)
	}
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.Message)
}

Node.js

const makeGrpcRequest = (JWT_AUTH_TOKEN, API_KEY, HOST, GREETEE) => {
  // Uncomment these lines to set their values
  // const JWT_AUTH_TOKEN = 'YOUR_JWT_AUTH_TOKEN';
  // const API_KEY = 'YOUR_API_KEY';
  // const HOST = 'localhost:50051'; // The IP address of your endpoints host
  // const GREETEE = 'world';

  // Import required libraries
  const grpc = require('grpc');
  const path = require('path');

  // Load protobuf spec for an example API
  const PROTO_PATH = path.join(__dirname, '/protos/helloworld.proto');
  const protoObj = grpc.load(PROTO_PATH).helloworld;

  // Create a client for the protobuf spec
  const client = new protoObj.Greeter(HOST, grpc.credentials.createInsecure());

  // Build gRPC request
  const metadata = new grpc.Metadata();
  if (API_KEY) {
    metadata.add('x-api-key', API_KEY);
  } else if (JWT_AUTH_TOKEN) {
    metadata.add('authorization', `Bearer ${JWT_AUTH_TOKEN}`);
  }

  // Execute gRPC request
  client.sayHello({name: GREETEE}, metadata, (err, response) => {
    if (err) {
      console.error(err);
    }

    if (response) {
      console.log(response.message);
    }
  });
};

HTTP クライアント

gRPC の HTTP コード変換機能に Cloud Endpoints を使用している場合、HTTP クライアントは OpenAPI サービスの場合と同じ方法でキーをクエリ パラメータとして送信できます。

API キーにより保護されている API を共有する

API キーは、作成された Google Cloud プロジェクトに関連付けられます。API で API キーを必須とするように設定すると、API キーが作成される Google Cloud プロジェクトは、次の質問に対する回答によって異なります。

  • API の個々の呼び出し元を互いに区別して、割り当てなどの Endpoints 機能を使用できるようにする必要があるか。
  • API のすべての呼び出し元に独自の Google Cloud プロジェクトがあるか。
  • 異なる API キー制限を設定する必要があるか。

どの Google Cloud プロジェクトで API キーを作成するかを決定するためのガイドとして、以下のディシジョン ツリーを使用できます。

API キー ディシジョン ツリー

API を有効にする権限を付与する

API の個々の呼び出し元を区別する必要があり、呼び出し元ごとに独自の Google Cloud プロジェクトがある場合、各自の Google Cloud プロジェクトで API を有効にする権限をプリンシパルに付与できます。これにより、ユーザーは API で使用するキーを各自で作成できます。

たとえば、社内のクライアント プログラムに独自の Google Cloud プロジェクトが関連付けられ、これらのプログラムが使用する API を作成しているとします。API の呼び出し元を区別するには、各呼び出し元の API キーをそれぞれの Google Cloud プロジェクトで作成する必要があります。この場合、クライアント プログラムの Google Cloud プロジェクトで API を有効にする権限をプロジェクトのメンバーに付与します。

ユーザーが独自の API キーを作成できるようにするには:

  1. API が構成されている Google Cloud プロジェクトで、API を有効にする権限を各ユーザーに付与します。
  2. 各ユーザーと連絡を取り、ユーザーの Google Cloud プロジェクトで API を有効化し、API キーを作成できるようになったことを知らせます。

呼び出し元ごとに個別の Google Cloud プロジェクトを作成する

API の個々の呼び出し元を区別する必要がある場合、すべての呼び出し元が Google Cloud プロジェクトを持っているとは限らない状況では、呼び出し元ごとに個別の Google Cloud プロジェクトと API キーを作成できます。プロジェクトを作成する前に、プロジェクトに関連付けられた呼び出し元を簡単に識別できるようにプロジェクト名について考慮してください。

たとえば、API を利用する外部ユーザーがあり、API を呼び出すクライアント プログラムの作成方法がわからないとします。たとえば、Google Cloud サービスを使用しているクライアントに Google Cloud プロジェクトがある場合もあれば、存在しない場合もあります。個々の呼び出し元を区別するには、呼び出し元ごとに個別の Google Cloud プロジェクトと API キーを作成する必要があります。

呼び出し元ごとに個別の Google Cloud プロジェクトと API キーを作成するには:

  1. 呼び出し元ごとに個別のプロジェクトを作成します。
  2. 各プロジェクトで、API を有効化して API キーを作成します。
  3. それぞれの呼び出し元に API キーを付与します。

呼び出し元ごとに API キーを作成する

API の呼び出し元を区別する必要はないものの、API キーの制限を追加する必要がある場合は、同じプロジェクト内で呼び出し元ごとに個別の API キーを作成できます。

同じプロジェクト内で呼び出し元ごとに API キーを作成するには:

  1. API が構成されているプロジェクトまたは API が有効化されているプロジェクトで、API キー制限が必要なユーザーごとに API キーを作成します。
  2. それぞれの呼び出し元に API キーを付与します。

すべての呼び出し元に同じ API キーを作成する

API の呼び出し元を区別する必要も、API の制限を追加する必要もないが、(匿名アクセスを防ぐなどの目的で)API キーを要求する場合は、すべての呼び出し元が使用する 1 つの API キーを作成できます。

すべての呼び出し元に同じ API キーを作成するには:
  1. API が構成されているプロジェクトまたは API が有効になっているプロジェクトで、すべての呼び出し元に使用する API キーを作成します。
  2. すべての呼び出し元に同じ API キーを付与します。

次のステップ