服务之间的身份验证

除了对最终用户请求进行身份验证之外,您可能还需要对向 API 发出请求的服务(非真人用户)进行身份验证。 本页面介绍如何使用服务账号为真人或服务提供身份验证。

概览

要识别向您的 API 发送请求的服务,请使用服务账号。 调用服务会使用服务账号的私钥来签署安全的 JSON Web 令牌 (JWT),并将已签名的 JWT 随请求发送到您的 API。

要在您的 API 和调用服务中实现服务账号身份验证,请执行以下操作:

  1. 为调用的服务创建服务账号和密钥。
  2. 在 API Gateway 服务的 API 配置中添加对身份验证的支持。
  3. 将代码添加到调用服务:

    • 创建 JWT 并使用服务账号的私钥为其签名。
    • 将签名的 JWT 随请求发送到 API。

API Gateway 会在将请求转发给您的 API 之前验证 JWT 中的声明是否与 API 配置中的配置相匹配。API Gateway 不会检查您已经在服务账号上授予的 Cloud Identity 权限。

前提条件

本页面假定您已经完成以下操作:

使用密钥创建服务账号

您需要一个包含调用服务用于签署 JWT 的私钥文件的服务账号。如果有多个服务向您的 API 发送请求,您可以创建一个服务账号来代表所有调用服务。如果您需要区分这些服务(例如,它们可能具有不同的权限),您可以为每个调用服务分别创建服务账号和密钥。

本部分介绍如何使用 Google Cloud 控制台和 gcloud 命令行工具创建服务帐号和私钥文件,以及如何为服务帐号分配 Service Account Token Creator 角色。如需了解如何使用 API 执行此任务,请参阅创建和管理服务账号

要使用密钥创建服务账号:

Google Cloud 控制台

创建服务账号:

  1. 在 Google Cloud 控制台中,前往创建服务帐号

    转到“创建服务账号”

  2. 选择一个项目。

  3. 服务账号名称字段中,输入一个名称。Google Cloud 控制台会根据此名称填充服务账号 ID 字段。

  4. 可选:在服务账号说明字段中,输入说明。

  5. 点击创建

  6. 点击选择角色字段。

    所有角色下,选择服务账号 > Service Account Token Creator

  7. 点击继续

  8. 点击完成以完成服务账号的创建过程。

    不要关闭浏览器窗口。您将在下一步骤中用到它。

创建服务账号密钥:

  1. 在 Google Cloud 控制台中,点击您创建的服务账号的电子邮件地址。
  2. 点击密钥
  3. 依次点击添加密钥创建新密钥
  4. 点击创建。JSON 密钥文件将下载到您的计算机上。
  5. 点击关闭

gcloud

您可以使用本地机器上的 Google Cloud CLI 或在 Cloud Shell 中运行以下命令。

  1. 设置 gcloud 的默认账号。如果您有多个账号,请务必选择您要使用的 Google Cloud 项目中的账号。

    gcloud auth login
    
  2. 显示您的 Google Cloud 项目的 ID:

    gcloud projects list
    
  3. 设置默认项目。将 PROJECT_ID 替换为您要使用的 Google Cloud 项目 ID。

    gcloud config set project PROJECT_ID
  4. 创建一个服务账号。将 SA_NAMESA_DISPLAY_NAME 替换为您要使用的名称和显示名。

    gcloud iam service-accounts create SA_NAME \
      --display-name "SA_DISPLAY_NAME"
    
  5. 显示刚刚创建的服务账号的电子邮件地址。

    gcloud iam service-accounts list
    
  6. 添加 Service Account Token Creator 角色。将 SA_EMAIL_ADDRESS 替换为服务账号的电子邮件地址。

    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member serviceAccount:SA_EMAIL_ADDRESS \
      --role roles/iam.serviceAccountTokenCreator
    
  7. 在当前工作目录中创建服务账号密钥文件。 将 FILE_NAME 替换为您要用于密钥文件的名称。默认情况下,gcloud 命令会创建一个 JSON 文件。

    gcloud iam service-accounts keys create FILE_NAME.json \
      --iam-account SA_EMAIL_ADDRESS
    

如需详细了解以上命令,请参阅 gcloud 参考

如需了解如何对私钥采取保护措施,请参阅管理凭据的最佳做法

配置 API 以支持身份验证

为网关创建 API 配置时,您需要指定网关用来与其他服务交互的服务账号。 为服务启用服务账号身份验证调用修改网关安全要求对象安全定义对象。按照以下步骤操作后,API Gateway 可以验证调用服务使用的已签名 JWT 中的声明。

  1. 在 API 配置中添加服务账号作为颁发者。

    securityDefinitions:
      DEFINITION_NAME:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "SA_EMAIL_ADDRESS"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/SA_EMAIL_ADDRESS"
    
    • DEFINITION_NAME 替换为标识此安全性定义的字符串。您可能希望将其替换为服务账号名称或标识调用服务的名称。
    • SA_EMAIL_ADDRESS 替换为服务账号的电子邮件地址。
    • 您可以在 API 配置中定义多个安全定义,但每个定义必须具有不同的 x-google-issuer。如果您为每个调用服务创建了单独的服务账号,那么您可以为每个服务账号创建安全性定义,例如:
    securityDefinitions:
      service-1:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "service-1@example-project-12345.iam.gserviceaccount.com"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/service-1@example-project-12345.iam.gserviceaccount.com"
      service-2:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "service-2@example-project-12345.iam.gserviceaccount.com"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/service-2@example-project-12345.iam.gserviceaccount.com"
    
  2. 您可以选择将 x-google-audiences 添加到 securityDefinitions 部分。如果您未添加 x-google-audiences,则 API Gateway 要求 JWT 中的 "aud"(受众群体)声明的格式为 https://SERVICE_NAME,其中 SERVICE_NAME 是 API 网关服务的名称,您已在 OpenAPI 文档的 host 字段中配置该服务。

  3. 在文件的顶层位置添加 security 部分(不缩进或嵌套)以应用于整个 API,或者在方法级层添加以应用于特定方法。如果同时在 API 级层和方法级层使用 security 部分,则方法级层的设置将替换 API 级层的设置。

    security:
      - DEFINITION_NAME: []
    
    • DEFINITION_NAME 替换为您在 securityDefinitions 部分中使用的名称。
    • 如果您在 securityDefinitions 部分有多个定义,请将它们添加到 security 部分,例如:

      security:
        - service-1: []
        - service-2: []
      
  4. 部署更新后的 API 配置

在 API Gateway 将请求转发到 API 之前,API Gateway 会验证以下几个方面:

  • 使用公钥来验证 JWT 的签名,该签名位于 API 配置的 x-google-jwks_uri 字段中指定的 URI 处。
  • 验证 JWT 中的 "iss"(颁发者)声明与 x-google-issuer 字段中指定的值是否匹配。
  • 验证 JWT 中的 "aud"(目标对象)声明是否包含您的 API Gateway 服务名称或与您在 x-google-audiences 字段中指定的任一值匹配。
  • 使用 "exp"(到期时间)声明来验证令牌是否未到期。

如需详细了解 x-google-issuerx-google-jwks_urix-google-audiences,请参阅 OpenAPI 扩展程序

向 API Gateway API 发出经过身份验证的请求

要发送经过身份验证的请求,调用服务会发送用您在 API 配置中指定的服务账号签名的 JWT。调用服务必须:

  1. 创建 JWT 并使用服务账号的私钥对其进行签名。
  2. 将签名的 JWT 随请求发送到 API。

以下示例代码演示了此过程如何选取语言。如要使用其他语言发出经过身份验证的请求,请参阅 jwt.io 以获取支持的库列表。

  1. 在调用服务中,添加以下函数并向其传递以下参数:
    Java
    • saKeyfile:服务账号私钥文件的完整路径。
    • saEmail:服务账号的电子邮件地址。
    • audience:如果您将 x-google-audiences 字段添加到 API 配置,请将 audience 设置为您为 x-google-audiences 指定的值之一。否则,请将 audience 设置为 https://SERVICE_NAME,其中 SERVICE_NAME 是您的 API Gateway 服务名称。
    • expiryLength:JWT 的到期时间,以秒为单位。
    Python
    • sa_keyfile:服务账号私钥文件的完整路径。
    • sa_email:服务账号的电子邮件地址。
    • audience:如果您将 x-google-audiences 字段添加到了 API 配置,请将 audience 设置为您为 x-google-audiences 指定的值之一。否则,请将 audience 设置为 https://SERVICE_NAME,其中 SERVICE_NAME 是您的 API Gateway 服务名称。
    • expiry_length:JWT 的到期时间,以秒为单位。
    Go
    • saKeyfile:服务账号私钥文件的完整路径。
    • saEmail:服务账号的电子邮件地址。
    • audience:如果您将 x-google-audiences 字段添加到了 API 配置,请将 audience 设置为您为 x-google-audiences 指定的值之一。否则,请将 audience 设置为 https://SERVICE_NAME,其中 SERVICE_NAME 是您的 API Gateway 服务名称。
    • expiryLength:JWT 的到期时间,以秒为单位。

    该函数会创建一个 JWT,使用私钥文件对其进行签名,并返回已签名的 JWT。

    Java
    /**
     * Generates a signed JSON Web Token using a Google API Service Account
     * utilizes com.auth0.jwt.
     */
    public static String generateJwt(final String saKeyfile, final String saEmail,
        final String audience, final int expiryLength)
        throws FileNotFoundException, IOException {
    
      Date now = new Date();
      Date expTime = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiryLength));
    
      // Build the JWT payload
      JWTCreator.Builder token = JWT.create()
          .withIssuedAt(now)
          // Expires after 'expiryLength' seconds
          .withExpiresAt(expTime)
          // Must match 'issuer' in the security configuration in your
          // swagger spec (e.g. service account email)
          .withIssuer(saEmail)
          // Must be either your Endpoints service name, or match the value
          // specified as the 'x-google-audience' in the OpenAPI document
          .withAudience(audience)
          // Subject and email should match the service account's email
          .withSubject(saEmail)
          .withClaim("email", saEmail);
    
      // Sign the JWT with a service account
      FileInputStream stream = new FileInputStream(saKeyfile);
      ServiceAccountCredentials cred = ServiceAccountCredentials.fromStream(stream);
      RSAPrivateKey key = (RSAPrivateKey) cred.getPrivateKey();
      Algorithm algorithm = Algorithm.RSA256(null, key);
      return token.sign(algorithm);
    }
    Python
    def generate_jwt(
        sa_keyfile,
        sa_email="account@project-id.iam.gserviceaccount.com",
        audience="your-service-name",
        expiry_length=3600,
    ):
        """Generates a signed JSON Web Token using a Google API Service Account."""
    
        now = int(time.time())
    
        # build payload
        payload = {
            "iat": now,
            # expires after 'expiry_length' seconds.
            "exp": now + expiry_length,
            # iss must match 'issuer' in the security configuration in your
            # swagger spec (e.g. service account email). It can be any string.
            "iss": sa_email,
            # aud must be either your Endpoints service name, or match the value
            # specified as the 'x-google-audience' in the OpenAPI document.
            "aud": audience,
            # sub and email should match the service account's email address
            "sub": sa_email,
            "email": sa_email,
        }
    
        # sign with keyfile
        signer = google.auth.crypt.RSASigner.from_service_account_file(sa_keyfile)
        jwt = google.auth.jwt.encode(signer, payload)
    
        return jwt
    
    
    Go
    
    // generateJWT creates a signed JSON Web Token using a Google API Service Account.
    func generateJWT(saKeyfile, saEmail, audience string, expiryLength int64) (string, error) {
    	now := time.Now().Unix()
    
    	// Build the JWT payload.
    	jwt := &jws.ClaimSet{
    		Iat: now,
    		// expires after 'expiryLength' seconds.
    		Exp: now + expiryLength,
    		// Iss must match 'issuer' in the security configuration in your
    		// swagger spec (e.g. service account email). It can be any string.
    		Iss: saEmail,
    		// Aud must be either your Endpoints service name, or match the value
    		// specified as the 'x-google-audience' in the OpenAPI document.
    		Aud: audience,
    		// Sub and Email should match the service account's email address.
    		Sub:           saEmail,
    		PrivateClaims: map[string]interface{}{"email": saEmail},
    	}
    	jwsHeader := &jws.Header{
    		Algorithm: "RS256",
    		Typ:       "JWT",
    	}
    
    	// Extract the RSA private key from the service account keyfile.
    	sa, err := ioutil.ReadFile(saKeyfile)
    	if err != nil {
    		return "", fmt.Errorf("Could not read service account file: %w", err)
    	}
    	conf, err := google.JWTConfigFromJSON(sa)
    	if err != nil {
    		return "", fmt.Errorf("Could not parse service account JSON: %w", err)
    	}
    	block, _ := pem.Decode(conf.PrivateKey)
    	parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    	if err != nil {
    		return "", fmt.Errorf("private key parse error: %w", err)
    	}
    	rsaKey, ok := parsedKey.(*rsa.PrivateKey)
    	// Sign the JWT with the service account's private key.
    	if !ok {
    		return "", errors.New("private key failed rsa.PrivateKey type assertion")
    	}
    	return jws.Encode(jwsHeader, jwt, rsaKey)
    }
    
  2. 在调用服务中,添加以下函数,将签名的 JWT 随请求的 Authorization: Bearer 标头发送到 API:
    Java
    /**
     * Makes an authorized request to the endpoint.
     */
    public static String makeJwtRequest(final String signedJwt, final URL url)
        throws IOException, ProtocolException {
    
      HttpURLConnection con = (HttpURLConnection) url.openConnection();
      con.setRequestMethod("GET");
      con.setRequestProperty("Content-Type", "application/json");
      con.setRequestProperty("Authorization", "Bearer " + signedJwt);
    
      InputStreamReader reader = new InputStreamReader(con.getInputStream());
      BufferedReader buffReader = new BufferedReader(reader);
    
      String line;
      StringBuilder result = new StringBuilder();
      while ((line = buffReader.readLine()) != null) {
        result.append(line);
      }
      buffReader.close();
      return result.toString();
    }
    Python
    def make_jwt_request(signed_jwt, url="https://your-endpoint.com"):
        """Makes an authorized request to the endpoint"""
        headers = {
            "Authorization": "Bearer {}".format(signed_jwt.decode("utf-8")),
            "content-type": "application/json",
        }
        response = requests.get(url, headers=headers)
        print(response.status_code, response.content)
        response.raise_for_status()
    
    
    Go
    
    // makeJWTRequest sends an authorized request to your deployed endpoint.
    func makeJWTRequest(signedJWT, url string) (string, error) {
    	client := &http.Client{
    		Timeout: 10 * time.Second,
    	}
    
    	req, err := http.NewRequest("GET", url, nil)
    	if err != nil {
    		return "", fmt.Errorf("failed to create HTTP request: %w", err)
    	}
    	req.Header.Add("Authorization", "Bearer "+signedJWT)
    	req.Header.Add("content-type", "application/json")
    
    	response, err := client.Do(req)
    	if err != nil {
    		return "", fmt.Errorf("HTTP request failed: %w", err)
    	}
    	defer response.Body.Close()
    	responseData, err := ioutil.ReadAll(response.Body)
    	if err != nil {
    		return "", fmt.Errorf("failed to parse HTTP response: %w", err)
    	}
    	return string(responseData), nil
    }
    

当您使用 JWT 发送请求时,出于安全原因,我们建议您将身份验证令牌放在 Authorization: Bearer 标头中。例如:

curl --request POST \
  --header "Authorization: Bearer ${TOKEN}" \
  "${GATEWAY_URL}/echo"

其中 GATEWAY_URLTOKEN 分别是包含已部署网关网址和身份验证令牌的环境变量。

在 API 中接收经过验证的结果

API Gateway 通常会转发它收到的所有标头。但是,当后端地址由 API 配置中的 x-google-backend 指定时,它会替换原来的 Authorization 标头。

API Gateway 会将 X-Apigateway-Api-Userinfo 中的身份验证结果发送到后端 API。我们建议您使用此标头,而不是原来的 Authorization 标头。此标头采用 base64url 编码,并且包含 JWT 载荷。

后续步骤