服務之間的驗證

除了驗證使用者要求,您可能還需要驗證向 API 提出要求的服務 (非人類使用者)。本頁面說明如何使用服務帳戶為使用者或服務提供驗證。

總覽

為了識別傳送要求至您 API 的服務,需要使用服務帳戶。呼叫服務會使用服務帳戶的私密金鑰來簽署安全的 JSON Web Token (JWT),並將要求中的已簽署 JWT 傳送到您的 API。

如要在 API 和呼叫服務中實作服務帳戶驗證,請按照下列步驟操作:

  1. 建立呼叫服務要使用的服務帳戶與金鑰。
  2. 在 API Gateway 服務的 API 設定中新增驗證支援。
  3. 將程式碼新增到呼叫服務,如此就會:

    • 建立 JWT 並以服務帳戶私密金鑰簽署。
    • 在要求中將已簽署的 JWT 傳送至 API。

API Gateway 會先驗證 JWT 中的憑證附加資訊是否符合 API 設定中的設定,再將要求轉送至您的 API。API Gateway 不會檢查該服務帳戶已授予的 Cloud Identity 權限。

必要條件

本頁假設您已具備以下條件:

使用金鑰建立服務帳戶

您會需要一個服務帳戶,這個服務帳戶需擁有呼叫服務用來簽署 JWT 的私密金鑰檔案。如果您有多個服務會向您的 API 發送要求,則可建立一個服務帳戶來代表所有的呼叫服務。如果您需要區別不同服務 (例如各服務具有的權限可能各不相同),就可以為每個呼叫服務建立服務帳戶和金鑰。

本節會示範如何使用 Google Cloud 主控台和 gcloud 指令列工具建立服務帳戶和私密金鑰檔案,並將服務帳戶憑證建立者角色指派給服務帳戶。如要瞭解如何使用 API 進行這項工作,請參閱「建立和管理服務帳戶」。

如何使用金鑰建立服務帳戶:

Google Cloud 控制台

建立服務帳戶:

  1. 前往 Google Cloud 控制台的「Create service account」(建立服務帳戶)

    前往「Create service account」(建立服務帳戶)

  2. 選取專案。

  3. 在「Service account name」(服務帳戶名稱) 欄位中輸入名稱。Google Cloud 控制台會根據這個名稱填入「Service account ID」欄位。

  4. 選用:在「服務帳戶說明」欄位中輸入說明。

  5. 按一下 [建立]。

  6. 按一下「請選擇角色」欄位。

    在「All roles」(所有角色) 下方,依序選取「Service Accounts」(服務帳戶) >「Service Account Token Creator」(服務帳戶憑證建立者)

  7. 按一下「繼續」

  8. 按一下「Done」(完成),即完成建立服務帳戶。

    請勿關閉瀏覽器視窗。您將在下一個程序中使用此值。

建立服務帳戶金鑰:

  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. 新增服務帳戶憑證建立者角色。將 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 設定中的 安全性需求物件安全性定義物件。按照下列步驟操作,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 Gateway 服務名稱,您已在 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:如果已在 API 設定中新增 x-google-audiences 欄位,請將 audience 設為您指定給 x-google-audiences 的其中一個值。或是將 audience 設為 https://SERVICE_NAME,其中 SERVICE_NAME 是您的 API Gateway 服務名稱。
    • expiryLength:JWT 的到期時間,以秒為單位。
    Python
    • sa_keyfile:服務帳戶私密金鑰檔案的完整路徑。
    • sa_email:服務帳戶的電子郵件地址。
    • audience:如果已在 API 設定中新增 x-google-audiences 欄位,請將 audience 設為您指定給 x-google-audiences 的其中一個值。或是將 audience 設為 https://SERVICE_NAME,其中 SERVICE_NAME 是您的 API Gateway 服務名稱。
    • expiry_length:JWT 的到期時間,以秒為單位。
    Go
    • saKeyfile:服務帳戶私密金鑰檔案的完整路徑。
    • saEmail:服務帳戶的電子郵件地址。
    • audience:如果已在 API 設定中新增 x-google-audiences 欄位,請將 audience 設為您指定給 x-google-audiences 的其中一個值。或是將 audience 設為 https://SERVICE_NAME,其中 SERVICE_NAME 是您的 API Gateway 服務名稱。
    • expiryLength:JWT 的到期時間,以秒為單位。

    此函式會建立一個 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 := os.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. 在呼叫服務中,新增下列函式,以在要求中使用 Authorization: Bearer 標頭傳送已簽署的 JWT 給 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 := io.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 酬載。

後續步驟