对用户进行身份验证

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

ESP 支持多种身份验证方法,包括 Firebase、Auth0 和 Google 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

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/grpc-js');
  const protoLoader = require('@grpc/proto-loader');
  const path = require('path');

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

  const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
  });

  const protoObj = grpc.loadPackageDefinition(packageDefinition).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。

后续步骤