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

בנוסף לאימות בקשות של משתמשי קצה, יכול להיות שתרצו לאמת שירותים (משתמשים לא אנושיים) ששולחים בקשות ל-API שלכם. בדף הזה מוסבר איך להשתמש בחשבונות שירות כדי לספק אימות לבני אדם או לשירותים.

סקירה כללית

כדי לזהות שירות ששולח בקשות ל-API שלכם, אתם משתמשים בחשבון שירות. שירות הקריאה משתמש במפתח הפרטי של חשבון השירות כדי לחתום על אסימון אינטרנט מאובטח מסוג JSON‏ (JWT) ושולח את ה-JWT החתום בבקשה ל-API שלכם.

כדי להטמיע אימות של חשבון שירות ב-API ובשירות שקורא ל-API:

  1. יוצרים חשבון שירות ומפתח לשימוש של השירות שקורא ל-API.
  2. מוסיפים תמיכה באימות בהגדרות ה-API של שירות API Gateway.
  3. מוסיפים קוד לשירות השיחות ש:

    • יוצרת JWT וחותמת עליו באמצעות המפתח הפרטי של חשבון השירות.
    • שליחת ה-JWT החתום בבקשה ל-API.

‫API Gateway מאמת שההצהרות ב-JWT תואמות להגדרה בקובץ ההגדרות של ה-API לפני שהוא מעביר את הבקשה ל-API. ‫API Gateway לא בודק את ההרשאות של Cloud Identity שנתתם בחשבון השירות.

דרישות מוקדמות

במאמר הזה אנחנו יוצאים מנקודת הנחה שכבר:

יצירת חשבון שירות עם מפתח

אתם צריכים חשבון שירות עם קובץ מפתח פרטי ששירות הקריאה משתמש בו כדי לחתום על ה-JWT. אם יש לכם יותר משירות אחד ששולח בקשות ל-API, אתם יכולים ליצור חשבון שירות אחד שייצג את כל השירותים ששולחים את הבקשות. אם אתם צריכים להבדיל בין השירותים – למשל, אם יש להם הרשאות שונות – אתם יכולים ליצור חשבון שירות ומפתח לכל שירות שקורא ל-API.

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

כדי ליצור חשבון שירות עם מפתח:

מסוף Google Cloud

יוצרים חשבון שירות:

  1. במסוף Google Cloud , נכנסים אל יצירת חשבון שירות.

    כניסה לדף Create service account

  2. בוחרים פרויקט.

  3. כותבים שם בשדה Service account name. השדה Service account ID ימולא במסוףGoogle Cloud בהתאם לשם הזה.

  4. אופציונלי: בשדה תיאור חשבון השירות, מזינים תיאור.

  5. לוחצים על יצירה.

  6. לוחצים על השדה בחירת תפקיד.

    בקטע All roles (כל התפקידים), בוחרים באפשרות Service Accounts (חשבונות שירות) > Service Account Token Creator (יצירת אסימונים בחשבון שירות).

  7. לוחצים על Continue.

  8. לוחצים על Done כדי לסיים ליצור את חשבון השירות.

    לא לסגור את חלון הדפדפן. תשתמשו בו גם בשלב הבא.

יוצרים מַפְתח לחשבון השירות:

  1. במסוף Google Cloud , לוחצים על כתובת האימייל של חשבון השירות שיצרתם.
  2. לוחצים על Keys.
  3. לוחצים על Add key ואז על Create new key.
  4. לוחצים על Create. למחשב שלכם תתבצע הורדה של קובץ JSON עם המפתח.
  5. לוחצים על Close.

gcloud

אפשר להריץ את הפקודות הבאות באמצעות Google Cloud CLI במחשב המקומי או ב-Cloud Shell.

  1. הגדרת חשבון ברירת המחדל ל-gcloud. אם יש לכם יותר מחשבון אחד, חשוב לבחור את החשבון שמשויך ל Google Cloud פרויקט שבו אתם רוצים להשתמש.

    gcloud auth login
  2. הצגת מזהי הפרויקטים של Google Cloud הפרויקטים שלכם.

    gcloud projects list
  3. מגדירים את פרויקט ברירת המחדל. מחליפים את PROJECT_ID במזהה הפרויקט שרוצים להשתמש בו. Google Cloud

    gcloud config set project PROJECT_ID
  4. יוצרים חשבון שירות. מחליפים את SA_NAME ואת SA_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. השינויים יהיו שונים בהתאם לגרסה של מפרט OpenAPI שבה נעשה שימוש.

כדי להגדיר את API Gateway כך שיאמת את ההצהרות באסימון ה-JWT החתום שבו משתמשים שירותים ששולחים קריאות:

OpenAPI 2.0

  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 דורש שההצהרה "aud" (קהל) באסימון ה-JWT תהיה בפורמט https://SERVICE_NAME, כאשר SERVICE_NAME הוא השם של שירות API Gateway שהגדרתם בשדה host במסמך OpenAPI.
  3. מוסיפים קטע security ברמה העליונה של הקובץ (לא מוזח או מקונן) כדי להחיל אותו על ה-API כולו, או ברמת השיטה כדי להחיל אותו על שיטה ספציפית. אם משתמשים בקטעי security ברמת ה-API וברמת השיטה, ההגדרות ברמת השיטה מבטלות את ההגדרות ברמת ה-API.
    security:
      - DEFINITION_NAME: []
    • מחליפים את DEFINITION_NAME בשם שבו השתמשתם בקטע securityDefinitions.
    • אם יש יותר מהגדרה אחת בקטע securityDefinitions, מוסיפים אותן בקטע security. לדוגמה:
      security:
        - service-1: []
        - service-2: []
  4. מבצעים פריסה של הגדרת ה-API המעודכנת. לפני ש-API Gateway מעביר בקשה ל-API שלכם, הוא בודק את הדברים הבאים:
    • החתימה של ה-JWT באמצעות המפתח הציבורי, שנמצא ב-URI שצוין בשדה x-google-jwks_uri בהגדרת ה-API.
    • התביעה "iss"(המנפיק) ב-JWT תואמת לערך שצוין בשדה x-google-issuer.
    • ההצהרה "aud"(קהל) ב-JWT מכילה את שם שירות ה-API Gateway או תואמת לאחד מהערכים שציינתם בשדה x-google-audiences.
    • שהתוקף של הטוקן לא פג באמצעות הטענה "exp"(זמן התפוגה).

‫OpenAPI 3.x

  1. מוסיפים את חשבון השירות כגורם מנפיק בהגדרת ה-API:
    components:
      securitySchemes:
        SCHEME_NAME:
          type: oauth2
          flows:
           implicit:
             authorizationUrl: ""
             scopes: {}
          x-google-auth:
            issuer: SA_EMAIL_ADDRESS
            jwksUri: https://www.googleapis.com/robot/v1/metadata/x509/SA_EMAIL_ADDRESS
    security:
      - SCHEME_NAME: []
    • מחליפים את SCHEME_NAME במחרוזת שמזהה את תוכנית האבטחה הזו. אפשר להחליף אותו בשם של חשבון השירות או בשם שמזהה את השירות שמבצע את הקריאה.
    • מחליפים את SA_EMAIL_ADDRESS בכתובת האימייל של חשבון השירות.
    • אפשר להגדיר כמה תוכניות אבטחה בהגדרות ה-API, אבל לכל הגדרה צריך להיות issuer שונה. אם יצרתם חשבונות שירות נפרדים לכל שירות שיחות, תוכלו ליצור הגדרת אבטחה לכל חשבון שירות, למשל:
      components:
        securitySchemes:
          service-1:
            type: oauth2
            flows:
             implicit:
               authorizationUrl: ""
               scopes: {}
            x-google-auth:
              issuer: "service-1@example-project-12345.iam.gserviceaccount.com"
              jwksUri: https://www.googleapis.com/robot/v1/metadata/x509/service-1@example-project-12345.iam.gserviceaccount.com
              jwtLocations:
                - header: Authorization
                  valuePrefix: "Bearer "
          service-2:
            type: oauth2
            flows:
             implicit:
               authorizationUrl: ""
               scopes: {}
            x-google-auth:
              issuer: "service-2@example-project-12345.iam.gserviceaccount.com"
              jwksUri: "https://www.googleapis.com/robot/v1/metadata/x509/service-2@example-project-12345.iam.gserviceaccount.com"
              
  2. אפשר גם להוסיף audiences לקטע securitySchemes. אם לא מוסיפים את audiences,‏ API Gateway דורש שההצהרה "aud" (קהל) באסימון ה-JWT תהיה בפורמט https://SERVICE_NAME, כאשר SERVICE_NAME הוא השם של שירות API Gateway שהגדרתם בשדה servers.url במסמך OpenAPI.
  3. מוסיפים קטע security ברמה העליונה של הקובץ (לא מוזח או מקונן) כדי להחיל אותו על ה-API כולו, או ברמת השיטה כדי להחיל אותו על שיטה ספציפית. אם משתמשים בקטעי security ברמת ה-API וברמת השיטה, ההגדרות ברמת השיטה מבטלות את ההגדרות ברמת ה-API.
    security:
      - SCHEME_NAME: []
    • מחליפים את SCHEME_NAME בשם שבו השתמשתם בקטע securitySchemes.
    • אם יש יותר מהגדרה אחת בקטע securitySchemes, מוסיפים אותן בקטע security. לדוגמה:
      security:
        - service-1: []
        - service-2: []
  4. פורסים את הגדרת ה-API המעודכנת. לפני ש-API Gateway מעביר בקשה ל-API שלכם, הוא בודק את הדברים הבאים:
    • החתימה של ה-JWT באמצעות המפתח הציבורי, שנמצא ב-URI שצוין בשדה jwksUri בהגדרת ה-API.
    • התביעה "iss"(המנפיק) ב-JWT תואמת לערך שצוין בשדה issuer.
    • ההצהרה "aud"(קהל) ב-JWT מכילה את שם שירות ה-API Gateway או תואמת לאחד מהערכים שציינתם בשדה audiences.
    • שהתוקף של הטוקן לא פג באמצעות הטענה "exp"(זמן התפוגה).

שליחת בקשה מאומתת ל-API של API Gateway

כדי לשלוח בקשה מאומתת, שירות הקורא שולח JWT שחתום על ידי חשבון השירות שציינתם בהגדרת ה-API. שירות השיחות צריך:

  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 := 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. בשירות שמבצע את הקריאה, מוסיפים את הפונקציה הבאה כדי לשלוח את ה-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 := 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/hello"

כאן, צריך להחליף את GATEWAY_URL ואת TOKEN בכתובת ה-URL של השער ובאסימון האימות שפרסתם, בהתאמה.

קבלת תוצאות מאומתות ב-API

בדרך כלל, API Gateway מעביר את כל הכותרות שהוא מקבל. עם זאת, הוא מחליף את הכותרת המקורית Authorization כשכתובת ה-backend מצוינת על ידי x-google-backend בהגדרת ה-API.

‫API Gateway ישלח את תוצאת האימות ב-X-Apigateway-Api-Userinfo אל ה-API של הבק-אנד. מומלץ להשתמש בכותרת הזו במקום בכותרת המקורית Authorization. הכותרת הזו היא base64url מקודדת ומכילה את המטען הייעודי של ה-JWT.

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