对用户进行身份验证

身份验证允许可扩展服务代理 (ESP) 识别调用服务方法的用户,然后根据此信息决定是否允许用户使用该方法(授权)。本页面介绍身份验证如何与 Cloud Endpoints 针对 gRPC 服务进行协作,包括如何在 gRPC 服务中配置 ESP 以支持经过身份验证的请求,以及如何从 gRPC 客户端调用经过身份验证的方法。

ESP 支持多种身份验证方法,包括 FirebaseAuth0Google ID 令牌,所有这些都可以作为 gRPC API 配置的一部分进行设置。在每种情况下,客户端都需要在其请求中提供标识性 JSON 网络令牌 (JWT)。ESP 代表您的 API 验证此令牌,因此您无需自己添加任何特殊的身份验证代码。

虽然身份验证和要求提供 API 密钥两种方法都可用于限制谁可以调用服务的方法,但它们二者提供的安全性级别并不相同,而且向被调用的服务提供的信息也是不同的。请参阅何时及为何使用 API 密钥,详细了解 API 密钥与身份验证之间的差异,以及什么情况下适合使用哪个方案。

如需查看使用身份验证的完整示例,请参阅使用服务帐号进行身份验证,该示例为我们的教程中的 Bookstore 服务添加身份验证功能。

为 ESP 配置身份验证

您可以使用 authentication 部分在其 gRPC 服务配置 YAML 文件中为 gRPC 服务配置 Endpoints 身份验证。您应指定身份验证方法和身份验证来源的详细信息作为 providers,其中:

  • rules 中的 id 值用于标识身份验证提供方:通常使用身份验证方法的名称,但并非强制要求。

  • issuer 值是所需令牌的颁发者,并由此指定了身份验证方法。

  • jwks_uri 值是提供方公钥的 URI,用于验证令牌。某些身份验证方法不要求您指定该值,例如 Google ID 令牌,此方法中 ESP 会自动获取该信息。

  • jwt_locations 用于定义提取 JWT 的位置。

您可以在同一文件中定义多个安全提供方,但每个提供方必须具有不同的 issuer。如需了解详情,请参阅 AuthProvider

您可以使用 rules 指定要使用这些身份验证要求的 API 方法,如 AuthenticationRule 所述。

以下示例显示如何针对受支持的身份验证方法在 gRPC 服务设置 ESP:

Firebase

要支持 Firebase 身份验证,请执行以下操作:

authentication:
  providers:
  - id: firebase
    jwks_uri: https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com
    # Replace FIREBASE-PROJECT-ID with your Firebase project ID
    issuer: https://securetoken.google.com/FIREBASE-PROJECT-ID
    audiences: "FIREBASE-PROJECT-ID"
    # Optional.
    jwt_locations:
    # expect header "jwt-header-foo": "jwt-prefix-foo<TOKEN>"
    - header: "jwt-header-foo"
      value_prefix: "jwt-prefix-foo"
    - query: "jwt_query_bar"
  rules:
  - selector: "*"
    requirements:
      - provider_id: firebase

auth0

要支持 Auth0 身份验证,请执行以下操作:

authentication:
  providers:
  - id: auth0_jwk
    # Replace YOUR-ACCOUNT-NAME with your service account's email address.
    issuer: https://YOUR-ACCOUNT-NAME.auth0.com/
    jwks_uri: "https://YOUR-ACCOUNT-NAME.auth0.com/.well-known/jwks.json"
    # Optional. Replace YOUR-CLIENT-ID with your client ID
    audiences: "YOUR-CLIENT-ID"
  rules:
  - selector: "*"
    requirements:
      - provider_id: auth0_jwk

Google ID 令牌

要支持使用 Google ID 令牌的身份验证,请执行以下操作:

authentication:
  providers:
  - id: google_id_token
    # This "issuer" field has to match the field "iss" in the JWT token.
     # Sometime it is "accounts.google.com".
    issuer: https://accounts.google.com
    # Optional. Replace YOUR-CLIENT-ID with your client ID
    audiences: "YOUR-CLIENT-ID"
  rules:
  - selector: "*"
    requirements:
      - provider_id: google_id_token

自定义

要支持自定义身份验证,请执行以下操作:

authentication:
  providers:
  - id: custom_auth_id
    # The value below should be unique
    issuer: issuer of the token
    jwks_uri: url to the public key
    # Optional. Replace YOUR-CLIENT-ID with your client ID
    audiences: "YOUR-CLIENT-ID"
 rules:
 - selector: "*"
   requirements:
     - provider_id: custom_auth_id

对于 Firebase 身份验证,audiences 是必填字段,并且必须是您的 Firebase 项目 ID。对于所有其他身份验证方法,都是可选的。ESP 接受格式为后端服务的所有 JWT,格式为https://SERVICE_NAME位于aud声明。要允许其他客户端 ID 访问后端服务,您可以在 audiences 字段中指定允许的客户端 ID,以英文逗号分隔各个值。然后 ESP 接受 JWT,其中包含 aud 声明中列入白名单的客户端 ID。

从 gRPC 调用经过身份验证的方法

如果方法需要身份验证,则 gRPC 客户端需要将身份验证令牌作为元数据与其方法调用一起传递,其中密钥是 authorization,值为 Bearer <JWT_TOKEN>。查看相关示例,了解调用以 Python、Node.js 或 Java 编写的 Bookstore 示例时如何执行此操作:

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;
  }
}

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);
    }
  });
};

客户端如何获取有效 JWT 以供发送取决于身份验证方法。

在您的 API 中接收身份验证结果

ESP 通常会转发收到的所有标头。但是,当后端地址由 OpenAPI 规范中的 x-google-backend 或 gRPC 服务配置中的 BackendRule 指定时,它会替换原来的 Authorization 标头。

ESP 会将 X-Endpoint-API-UserInfo 中的身份验证结果发送到后端 API。我们建议您使用此标头,而不是原来的 Authorization 标头。此标头是一个字符串。base64url对 JSON 对象进行编码。ESPv2 和 ESP 的 JSON 对象格式有所不同。对于 ESPv2,JSON 对象恰好是原始 JWT 载荷。对于 ESP,JSON 对象使用不同的字段名称,并将原始 JWT 载荷放在 claims 字段下。如需详细了解格式,请参阅处理后端服务中的 JWT

后续步骤