אימות בין שירותים

אם הארכיטקטורה שלכם משתמשת בכמה שירותים, סביר להניח שהשירותים האלה צריכים לתקשר זה עם זה, באמצעים אסינכרוניים או סינכרוניים. הרבה מהשירותים האלה יכולים להיות פרטיים, ולכן צריך פרטי כניסה כדי לגשת אליהם.

לתקשורת אסינכרונית, אתם יכולים להשתמש בשירותי Google Cloud הבאים:

  • ‫Cloud Tasks לתקשורת אסינכרונית של אחד לאחד
  • ‫Pub/Sub לתקשורת אסינכרונית של אחד לרבים, אחד לאחד ורבים לאחד
  • ‫Cloud Scheduler לתקשורת אסינכרונית מתוזמנת באופן קבוע
  • ‫Eventarc לתקשורת מבוססת-אירועים

בכל המקרים האלה, השירות שבו משתמשים מנהל את האינטראקציה עם השירות המקבל, על סמך ההגדרות שקבעתם.

אבל בתקשורת סינכרונית, השירות שלכם קורא לשירות אחר ישירות, באמצעות HTTP, דרך כתובת ה-URL של נקודת הקצה שלו. בתרחיש לדוגמה הזה, צריך לוודא שכל שירות יכול לשלוח בקשות רק לשירותים ספציפיים. לדוגמה, אם יש לכם שירות login, הוא אמור להיות מסוגל לגשת לשירות user-profiles, אבל לא לשירות search.

במצב כזה, Google ממליצה להשתמש ב-IAM ובזהות שירות שמבוססת על חשבון שירות מנוהל על ידי משתמשים לכל שירות, שקיבל את קבוצת ההרשאות המינימלית שנדרשת כדי לבצע את העבודה שלו.

בנוסף, הבקשה צריכה להציג הוכחה לזהות של השירות שמבצע את הקריאה. כדי לעשות את זה, צריך להגדיר את השירות שמבצע את הקריאה כך שיוסיף לבקשה טוקן של מזהה OpenID Connect שחתום על ידי Google.

הגדרת חשבון השירות

כדי להגדיר חשבון שירות, אתם צריכים להגדיר את השירות המקבל כך שיקבל בקשות מהשירות הקורא. לשם כך, אתם צריכים להפוך את חשבון השירות של השירות הקורא לחשבון משתמש בשירות המקבל. אחר כך מקצים לחשבון השירות את התפקיד Cloud Run Invoker‏ (roles/run.invoker). כדי לבצע את שתי המשימות האלה, פועלים לפי ההוראות בכרטיסייה המתאימה:

ממשק המשתמש של המסוף

  1. נכנסים למסוף Google Cloud :

    כניסה למסוף Google Cloud

  2. בוחרים את השירות המקבל.

  3. כדי להציג את הכרטיסייה Permissions, לוחצים על Show Info Panel בפינה הימנית העליונה.

  4. לוחצים על Add principal.

    1. מזינים את הזהות של השירות שמבצע את הקריאה. בדרך כלל מדובר בכתובת אימייל, כברירת מחדל PROJECT_NUMBER-compute@developer.gserviceaccount.com.

    2. בוחרים את התפקיד Cloud Run Invoker בתפריט הנפתח Select a role.

    3. לוחצים על Save.

gcloud

משתמשים בפקודה gcloud run services add-iam-policy-binding:

gcloud run services add-iam-policy-binding RECEIVING_SERVICE \
  --member='serviceAccount:CALLING_SERVICE_IDENTITY' \
  --role='roles/run.invoker'

כאשר RECEIVING_SERVICE הוא השם של השירות המקבל, ו-CALLING_SERVICE_IDENTITY הוא כתובת האימייל של חשבון השירות, כברירת מחדל PROJECT_NUMBER-compute@developer.gserviceaccount.com.

Terraform

כדי ללמוד איך להחיל הגדרות ב-Terraform או להסיר אותן, ראו פקודות בסיסיות ב-Terraform.

מוסיפים את הקוד הבא למשאב google_cloud_run_v2_service בתצורה של Terraform:

resource "google_cloud_run_v2_service" "public" {
  name     = "public-service"
  location = "us-central1"

  deletion_protection = false # set to "true" in production

  template {
    containers {
      # TODO<developer>: replace this with a public service container
      # (This service can be invoked by anyone on the internet)
      image = "us-docker.pkg.dev/cloudrun/container/hello"

      # Include a reference to the private Cloud Run
      # service's URL as an environment variable.
      env {
        name  = "URL"
        value = google_cloud_run_v2_service.private.uri
      }
    }
    # Give the "public" Cloud Run service
    # a service account's identity
    service_account = google_service_account.default.email
  }
}

מחליפים את us-docker.pkg.dev/cloudrun/container/hello בהפניה לקובץ האימג' של הקונטיינר.

קוד Terraform הבא הופך את השירות הראשוני לציבורי.

data "google_iam_policy" "public" {
  binding {
    role = "roles/run.invoker"
    members = [
      "allUsers",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "public" {
  location = google_cloud_run_v2_service.public.location
  project  = google_cloud_run_v2_service.public.project
  service  = google_cloud_run_v2_service.public.name

  policy_data = data.google_iam_policy.public.policy_data
}

הקוד הבא ב-Terraform יוצר שירות Cloud Run שני, שנועד להיות פרטי.

resource "google_cloud_run_v2_service" "private" {
  name     = "private-service"
  location = "us-central1"

  deletion_protection = false # set to "true" in production

  template {
    containers {
      // TODO<developer>: replace this with a private service container
      // (This service should only be invocable by the public service)
      image = "us-docker.pkg.dev/cloudrun/container/hello"
    }
  }
}

מחליפים את us-docker.pkg.dev/cloudrun/container/hello בהפניה לקובץ האימג' של הקונטיינר.

קוד Terraform הבא יוצר חשבון שירות.

resource "google_service_account" "default" {
  account_id   = "cloud-run-interservice-id"
  description  = "Identity used by a public Cloud Run service to call private Cloud Run services."
  display_name = "cloud-run-interservice-id"
}

קוד ה-Terraform הבא הופך את השירות השני לפרטי, על ידי מתן הרשאה להפעיל אותו רק לחשבון השירות שמבצע את הקריאה החוזרת.

data "google_iam_policy" "private" {
  binding {
    role = "roles/run.invoker"
    members = [
      "serviceAccount:${google_service_account.default.email}",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "private" {
  location = google_cloud_run_v2_service.private.location
  project  = google_cloud_run_v2_service.private.project
  service  = google_cloud_run_v2_service.private.name

  policy_data = data.google_iam_policy.private.policy_data
}

קבלת הטוקן של המזהה והגדרתו

אחרי שמקצים את התפקיד המתאים לחשבון השירות שמבצע את הקריאה, פועלים לפי השלבים הבאים:

  1. משיגים טוקן של מזהה שחתום על ידי Google, באמצעות אחת מהשיטות שמתוארות בקטע הבא. מגדירים את טענת הקהל (aud) לכתובת ה-URL של השירות המקבל או לקהל מותאם אישית שהוגדר. אם אתם לא משתמשים בקהל בהתאמה אישית, הערך של aud חייב להישאר כתובת ה-URL של השירות, גם כששולחים בקשות לתג תנועה ספציפי.

  2. מוסיפים את טוקן המזהה שקיבלתם בשלב הקודם לאחת מהכותרות הבאות בבקשה לשירות המקבל:

    • כותרת Authorization: Bearer ID_TOKEN.
    • כותרת X-Serverless-Authorization: Bearer ID_TOKEN. אפשר להשתמש בכותרת הזו אם האפליקציה כבר משתמשת בכותרת Authorization להרשאה בהתאמה אישית. הפעולה הזו מסירה את החתימה לפני העברת הטוקן למאגר של המשתמש.

במאמר שיטות לקבלת טוקן של מזהה תוכלו לקרוא על דרכים אחרות לקבלת טוקנים של מזהים שלא מתוארות בדף הזה.

שימוש בספריות לאימות

אחת מהדרכים לקבל ולהגדיר את התהליך של טוקן של מזהה היא להשתמש בספריות האימות. הקוד הזה פועל בכל סביבה, גם מחוץ ל- Google Cloud, שבה הספריות יכולות לקבל פרטי כניסה לאימות של חשבון שירות. כדי להשתמש בשיטה הזו, מורידים קובץ מפתח של חשבון שירות ומגדירים את משתנה הסביבה GOOGLE_APPLICATION_CREDENTIALS לנתיב של קובץ המפתח של חשבון השירות. למידע נוסף, קראו את המאמר מפתח של חשבון שירות.

הקוד הזה לא מקבל פרטי כניסה לאימות של חשבון משתמש.

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// Example: https://my-cloud-run-service.run.app/books/delete/12345
// const url = 'https://TARGET_HOSTNAME/TARGET_URL';

// Example (Cloud Run): https://my-cloud-run-service.run.app/
// const targetAudience = 'https://TARGET_AUDIENCE/';

const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();

async function request() {
  console.info(`request ${url} with target audience ${targetAudience}`);
  const client = await auth.getIdTokenClient(targetAudience);

  // Alternatively, one can use `client.idTokenProvider.fetchIdToken`
  // to return the ID Token.
  const res = await client.fetch(url);
  console.info(res.data);
}

request().catch(err => {
  console.error(err.message);
  process.exitCode = 1;
});

Python

import urllib

import google.auth.transport.requests
import google.oauth2.id_token


def make_authorized_get_request(endpoint, audience):
    """
    make_authorized_get_request makes a GET request to the specified HTTP endpoint
    by authenticating with the ID token obtained from the google-auth client library
    using the specified audience value.
    """

    # Cloud Run uses your service's hostname as the `audience` value
    # audience = 'https://my-cloud-run-service.run.app/'
    # For Cloud Run, `endpoint` is the URL (hostname + path) receiving the request
    # endpoint = 'https://my-cloud-run-service.run.app/my/awesome/url'

    req = urllib.request.Request(endpoint)

    auth_req = google.auth.transport.requests.Request()
    id_token = google.oauth2.id_token.fetch_id_token(auth_req, audience)

    req.add_header("Authorization", f"Bearer {id_token}")
    response = urllib.request.urlopen(req)

    return response.read()

Go


import (
	"context"
	"fmt"
	"io"

	"google.golang.org/api/idtoken"
)

// `makeGetRequest` makes a request to the provided `targetURL`
// with an authenticated client using audience `audience`.
func makeGetRequest(w io.Writer, targetURL string, audience string) error {
	// Example `audience` value (Cloud Run): https://my-cloud-run-service.run.app/
	// (`targetURL` and `audience` will differ for non-root URLs and GET parameters)
	ctx := context.Background()

	// client is a http.Client that automatically adds an "Authorization" header
	// to any requests made.
	client, err := idtoken.NewClient(ctx, audience)
	if err != nil {
		return fmt.Errorf("idtoken.NewClient: %w", err)
	}

	resp, err := client.Get(targetURL)
	if err != nil {
		return fmt.Errorf("client.Get: %w", err)
	}
	defer resp.Body.Close()
	if _, err := io.Copy(w, resp.Body); err != nil {
		return fmt.Errorf("io.Copy: %w", err)
	}

	return nil
}

Java

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdTokenCredentials;
import com.google.auth.oauth2.IdTokenProvider;
import java.io.IOException;

public class Authentication {

  // makeGetRequest makes a GET request to the specified Cloud Run or
  // Cloud Functions endpoint `serviceUrl` (must be a complete URL), by
  // authenticating with an ID token retrieved from Application Default
  // Credentials using the specified `audience`.
  //
  // Example `audience` value (Cloud Run): https://my-cloud-run-service.run.app/
  public static HttpResponse makeGetRequest(String serviceUrl, String audience) throws IOException {
    GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
    if (!(credentials instanceof IdTokenProvider)) {
      throw new IllegalArgumentException("Credentials are not an instance of IdTokenProvider.");
    }
    IdTokenCredentials tokenCredential =
        IdTokenCredentials.newBuilder()
            .setIdTokenProvider((IdTokenProvider) credentials)
            .setTargetAudience(audience)
            .build();

    GenericUrl genericUrl = new GenericUrl(serviceUrl);
    HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(tokenCredential);
    HttpTransport transport = new NetHttpTransport();
    HttpRequest request = transport.createRequestFactory(adapter).buildGetRequest(genericUrl);
    return request.execute();
  }
}

שימוש בשרת המטא-נתונים

אם מסיבה כלשהי אי אפשר להשתמש בספריות האימות, אפשר לאחזר טוקן של מזהה משרת המטא-נתונים של Compute בזמן שהקונטיינר פועל ב-Cloud Run. חשוב לזכור שהשיטה הזו לא פועלת מחוץ ל- Google Cloud, כולל מהמכונה המקומית.

curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=[AUDIENCE]" \
     -H "Metadata-Flavor: Google"

כאשר AUDIENCE היא כתובת ה-URL של השירות שאתם מפעילים או קהל בהתאמה אישית שהוגדר.

בטבלה הבאה מפורטים החלקים העיקריים של בקשת שאילתה למטא-נתונים:

רכיבים תיאור
כתובת URL בסיסית

כל ערכי המטא-נתונים מוגדרים כנתיבי משנה מתחת לכתובת ה-URL הבסיסית הבאה:

http://metadata.google.internal/computeMetadata/v1
Request header

הכותרת הבאה חייבת להופיע בכל בקשה:

Metadata-Flavor: Google

הכותרת הזו מציינת שהבקשה נשלחה במטרה לאחזר ערכי מטא-נתונים, ולא נשלחה בטעות ממקור לא מאובטח, ומאפשרת לשרת המטא-נתונים להחזיר את הנתונים שביקשתם. אם לא תספקו את הכותרת הזו, שרת המטא-נתונים ידחה את הבקשה.

במדריך לאבטחת שירותי Cloud Run תוכלו למצוא הסבר מפורט על אפליקציה שמשתמשת בשיטת האימות הזו בין שירותים.

שימוש באיחוד זהויות של עומסי עבודה מחוץ ל- Google Cloud

אם הסביבה שלכם משתמשת בספק זהויות שנתמך על ידי איחוד שירותי אימות הזהות של עומסי עבודה, אתם יכולים להשתמש בשיטה הבאה כדי לאמת באופן מאובטח את שירות Cloud Run מחוץ ל- Google Cloud:

  1. מגדירים את חשבון השירות כמו שמתואר בקטע הגדרת חשבון השירות בדף הזה.

  2. מגדירים את איחוד שירותי אימות הזהות של עומסי עבודה עבור ספק הזהויות, כפי שמתואר במאמר הגדרת איחוד שירותי אימות הזהות של עומסי עבודה.

  3. פועלים לפי ההוראות בקטע הענקת גישה לזהויות חיצוניות כדי להתחזות לחשבון שירות.

  4. משתמשים ב-API בארכיטקטורת REST כדי לקבל טוקן לטווח קצר, אבל במקום להפעיל את generateAccessToken כדי לקבל טוקן גישה, מפעילים את generateIdToken כדי לקבל טוקן של מזהה.

    לדוגמה, באמצעות cURL:

    ID_TOKEN=$(curl -0 -X POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/SERVICE_ACCOUNT:generateIdToken \
      -H "Content-Type: text/json; charset=utf-8" \
      -H "Authorization: Bearer $STS_TOKEN" \
      -d @- <<EOF | jq -r .token
      {
          "audience": "SERVICE_URL"
      }
    EOF
    )
    echo $ID_TOKEN

    ‫SERVICE_ACCOUNT היא כתובת האימייל של חשבון השירות שלמאגר הזהויות של עומסי העבודה יש גישה אליו, ו-SERVICE_URL היא כתובת ה-URL של שירות Cloud Run שאתם מפעילים. הערך הזה צריך להישאר כתובת ה-URL של השירות, גם כששולחים בקשות לתג תנועה ספציפי. ‫$STS_TOKEN הוא האסימון של Security Token Service שקיבלתם בשלב הקודם בהוראות של איחוד שירותי אימות הזהות של עומסי עבודה.

כדי לכלול את הטוקן של המזהה מהשלב הקודם בבקשה לשירות, אפשר להשתמש בכותרת Authorization: Bearer ID_TOKEN או בכותרת X-Serverless-Authorization: Bearer ID_TOKEN. אם שתי הכותרות מסופקות, רק הכותרת X-Serverless-Authorization נבדקת.

שימוש במפתח של חשבון שירות שהורדתם מחוץ ל- Google Cloud

אם איחוד שירותי אימות הזהות של עומסי עבודה לא מתאים לסביבה שלכם, אתם יכולים להשתמש במפתח שהורדתם של חשבון שירות כדי לבצע אימות מחוץ ל-Google Cloud. מעדכנים את קוד הלקוח כך שישתמש בספריות האימות, כמו שמתואר למעלה. למידע נוסף, קראו את המאמר מפתח של חשבון שירות.

אפשר לקבל טוקן של מזהה בחתימת Google באמצעות JWT בחתימה עצמית, אבל זה די מסובך ועלול לגרום לשגיאות. אלה השלבים הבסיסיים:

  1. חותמים חתימה עצמית על JWT של חשבון שירות, כשהטענה target_audience מוגדרת לכתובת ה-URL של השירות המקבל או לקהל בהתאמה אישית שהוגדר. אם לא משתמשים בדומיינים בהתאמה אישית, הערך של target_audience צריך להישאר כתובת ה-URL של השירות, גם כששולחים בקשות לתג תנועה ספציפי.

  2. מחליפים את ה-JWT בחתימה עצמית בטוקן של מזהה בחתימה של Google, שבו ההצהרה aud צריכה להיות מוגדרת לכתובת ה-URL הקודמת.

  3. מוסיפים את הטוקן של המזהה לבקשה לשירות באמצעות הכותרת Authorization: Bearer ID_TOKEN או הכותרת X-Serverless-Authorization: Bearer ID_TOKEN. אם שתי הכותרות צוינו, רק הכותרת X-Serverless-Authorization נבדקת.

קבלת בקשות מאומתות

בשירות הפרטי שמקבל את הבקשה, אפשר לנתח את כותרת ההרשאה כדי לקבל את המידע שנשלח על ידי הטוקן למוכ"ז.

Python

from flask import Request

from google.auth.exceptions import GoogleAuthError
from google.auth.transport import requests
from google.oauth2 import id_token


def receive_request_and_parse_auth_header(request: Request) -> str:
    """Parse the authorization header, validate the Bearer token
    and decode the token to get its information.

    Args:
        request: Flask request object.

    Returns:
        One of the following:
        a) The email from the request's Authorization header.
        b) A welcome message for anonymous users.
        c) An error description.
    """
    auth_header = request.headers.get("Authorization")
    if auth_header:
        # Split the auth type and value from the header.
        auth_type, creds = auth_header.split(" ", 1)

        if auth_type.lower() == "bearer":
            # Find more information about `verify_token` function here:
            # https://google-auth.readthedocs.io/en/master/reference/google.oauth2.id_token.html#google.oauth2.id_token.verify_token
            try:
                decoded_token = id_token.verify_token(creds, requests.Request())
                return f"Hello, {decoded_token['email']}!\n"
            except GoogleAuthError as e:
                return f"Invalid token: {e}\n"
        else:
            return f"Unhandled header format ({auth_type}).\n"

    return "Hello, anonymous user.\n"

Java


import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.http.apache.v2.ApacheHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import java.util.Arrays;
import java.util.Collection;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class Authentication {
  @RestController
  class AuthenticationController {

    private final AuthenticationService authService = new AuthenticationService();

    @GetMapping("/")
    public ResponseEntity<String> getEmailFromAuthHeader(
        @RequestHeader(value = "Authorization", required = false) String authHeader) {
      String responseBody;
      if (authHeader == null) {
        responseBody = "Error verifying ID token: missing Authorization header";
        return new ResponseEntity<>(responseBody, HttpStatus.UNAUTHORIZED);
      }

      String email = authService.parseAuthHeader(authHeader);
      if (email == null) {
        responseBody = "Unauthorized request. Please supply a valid bearer token.";
        HttpHeaders headers = new HttpHeaders();
        headers.add("WWW-Authenticate", "Bearer");
        return new ResponseEntity<>(responseBody, headers, HttpStatus.UNAUTHORIZED);
      }

      responseBody = "Hello, " + email;
      return new ResponseEntity<>(responseBody, HttpStatus.OK);
    }
  }

  public class AuthenticationService {
    /*
     * Parse the authorization header, validate and decode the Bearer token.
     *
     * Args:
     *    authHeader: String of HTTP header with a Bearer token.
     *
     * Returns:
     *    A string containing the email from the token.
     *    null if the token is invalid or the email can't be retrieved.
     */
    public String parseAuthHeader(String authHeader) {
      // Split the auth type and value from the header.
      String[] authHeaderStrings = authHeader.split(" ");
      if (authHeaderStrings.length != 2) {
        System.out.println("Malformed Authorization header");
        return null;
      }
      String authType = authHeaderStrings[0];
      String tokenValue = authHeaderStrings[1];
      // Validate and decode the ID token in the header.
      if (!"bearer".equals(authType.toLowerCase())) {
        System.out.println("Unhandled header format: " + authType);
        return null;
      }

      // Get the service URL from the environment variable
      // set at the time of deployment.
      String serviceUrl = System.getenv("SERVICE_URL");
      // Define the expected audience as the Service Base URL.
      Collection<String> audience = Arrays.asList(serviceUrl);

      try {
        // Find more information about the verification process in:
        // https://developers.google.com/identity/sign-in/web/backend-auth#java
        // https://cloud.google.com/java/docs/reference/google-api-client/latest/com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
        GoogleIdTokenVerifier verifier =
            new GoogleIdTokenVerifier.Builder(new ApacheHttpTransport(), new GsonFactory())
                .setAudience(audience)
                .build();
        GoogleIdToken googleIdToken = verifier.verify(tokenValue);

        // More info about the structure for the decoded ID Token here:
        // https://cloud.google.com/docs/authentication/token-types#id
        // https://cloud.google.com/java/docs/reference/google-api-client/latest/com.google.api.client.googleapis.auth.oauth2.GoogleIdToken
        // https://cloud.google.com/java/docs/reference/google-api-client/latest/com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload
        GoogleIdToken.Payload payload = googleIdToken.getPayload();
        if (!payload.getEmailVerified()) {
          System.out.println("Invalid token. Email wasn't verified.");
          return null;
        }
        System.out.println("Email verified: " + payload.getEmail());
        return payload.getEmail();

      } catch (Exception exception) {
        System.out.println("Ivalid token: " + exception);
      }
      return null;
    }
  }

  public static void main(String[] args) {
    SpringApplication.run(Authentication.class, args);
  }

המאמרים הבאים