서비스 간 인증

최종 사용자 요청을 인증하는 것 외에도 API에 요청을 수행하는 서비스(비인간 사용자)를 인증해야 할 수 있습니다. 이 페이지에서는 서비스 계정을 사용해서 인간 또는 서비스에 대해 인증을 제공하는 방법을 설명합니다.

개요

API에 요청을 보내는 서비스를 식별하려면 서비스 계정을 사용합니다. 호출하는 서비스는 서비스 계정의 비공개 키를 사용하여 보안 JSON 웹 토큰(JWT)에 서명하고, 서명된 JWT를 요청에 포함하여 API에 보냅니다.

API 및 호출 서비스에서 서비스 계정 인증을 구현하려면 다음 안내를 따르세요.

  1. 호출 서비스에서 사용할 서비스 계정과 키를 만듭니다.
  2. API 게이트웨이 서비스에 대해 API 구성에 인증 지원을 추가합니다.
  3. 호출 서비스에 다음과 같은 코드를 추가합니다.

    • JWT를 만들고 이를 서비스 계정의 비공개 키로 서명합니다.
    • 서명된 JWT를 API에 대한 요청에 포함하여 전송합니다.

API 게이트웨이는 요청을 API로 전달하기 전 JWT의 클레임이 API 구성 내용과 일치하는지 확인합니다. API 게이트웨이는 서비스 계정에 부여한 Cloud Identity 권한을 확인하지 않습니다.

기본 요건

이 페이지에서는 다음 작업을 이미 완료했다고 가정합니다.

키가 있는 서비스 계정 만들기

호출 서비스의 JWT 서명을 위한 비공개 키 파일을 보유한 서비스 계정이 필요합니다. API에 요청을 보내는 서비스가 두 개 이상이면 이 호출 서비스를 모두 포함하는 하나의 서비스 계정을 만들 수 있습니다. 서비스를 구분해야 하는 경우(예를 들어 서비스마다 사용 권한이 다른 경우) 호출 서비스별로 서비스 계정과 키를 만들 수 있습니다.

이 섹션에서는 Google Cloud Console 및 gcloud 명령줄 도구를 사용하여 서비스 계정 및 비공개 키 파일을 만들고 서비스 계정을 서비스 계정 토큰 작성기 역할에 할당하는 방법을 보여줍니다. API를 사용하여 이 작업을 수행하는 방법에 대한 자세한 내용은 서비스 계정 만들기 및 관리를 참조하세요.

키가 있는 서비스 계정을 만들려면 다음 안내를 따르세요.

Cloud Console

서비스 계정을 만듭니다.

  1. Cloud Console에서 서비스 계정 만들기로 이동합니다.

    서비스 계정 만들기로 이동

  2. 프로젝트를 선택합니다.

  3. 서비스 계정 이름 필드에 이름을 입력합니다. Cloud Console은 이 이름을 기반으로 서비스 계정 ID 필드를 채웁니다.

  4. 선택사항: 서비스 계정 설명 필드에 설명을 입력합니다.

  5. 만들기를 클릭합니다.

  6. 역할 선택 필드를 클릭합니다.

    모든 역할에서 서비스 계정 > 서비스 계정 토큰 생성자를 선택합니다.

  7. 계속을 클릭합니다.

  8. 완료를 클릭하여 서비스 계정 만들기를 마칩니다.

    브라우저 창을 닫지 마세요. 다음 절차에서 사용합니다.

서비스 계정 키 만들기

  1. Cloud Console에서 만든 서비스 계정의 이메일 주소를 클릭합니다.
  2. 를 클릭합니다.
  3. 키 추가를 클릭한 후 새 키 만들기를 클릭합니다.
  4. 만들기를 클릭합니다. JSON 키 파일이 컴퓨터에 다운로드됩니다.
  5. 닫기를 클릭합니다.

gcloud

로컬 머신이나 Cloud Shell 내에서 Cloud SDK를 사용하여 다음 명령어를 실행할 수 있습니다.

  1. gcloud에 기본 계정을 설정합니다. 계정이 2개 이상이면 사용하려는 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 구성에서 보안 요구사항 객체보안 정의 객체를 수정합니다. 아래 단계를 수행하면 호출 서비스에 사용되는 서명된 JWT에서 클레임을 검증하도록 API 게이트웨이가 사용 설정됩니다.

  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-audiencessecurityDefinitions 섹션에 추가합니다. x-google-audiences를 추가하지 않은 경우 API 게이트웨이에서는 JWT의 "aud"(대상) 클레임이 https://SERVICE_NAME 형식이어야 합니다. 여기서 SERVICE_NAME은 API 게이트웨이 서비스의 이름이며, OpenAPI 문서의 host 필드에 구성한 것입니다.

  3. security 섹션을 파일의 최상위 수준(들여쓰거나 중첩하지 않음)에서 추가하여 전체 API에 적용하거나 메서드 수준에서 추가하여 특정 메서드에 적용합니다. API 및 메서드 수준 모두에 security 섹션을 사용하는 경우에는 메서드 수준 설정이 API 수준 설정보다 우선 적용됩니다.

    security:
      - DEFINITION_NAME: []
    
    • DEFINITION_NAMEsecurityDefinitions 섹션에 사용한 이름으로 바꿉니다.
    • securityDefinitions 섹션에 정의가 둘 이상 있는 경우 security 섹션에 이를 추가합니다. 예를 들면 다음과 같습니다.

      security:
        - service-1: []
        - service-2: []
      
  4. 업데이트된 API 구성을 배포합니다.

API 게이트웨이는 요청을 API로 전달하기 전에 다음을 확인합니다.

  • 공개 키를 사용한 JWT의 서명이 API 구성의 x-google-jwks_uri 필드에 지정된 URI에 있는지 확인합니다.
  • JWT의 "iss"(발급기관) 클레임이 x-google-issuer 필드에 지정된 값과 일치하는지 확인합니다.
  • JWT의 "aud"(대상) 클레임이 API 게이트웨이 서비스 이름에 포함되었는지, x-google-audiences 필드에서 지정한 값 중 하나와 일치하는지 확인합니다.
  • "exp"(만료 시간) 클레임을 사용하여 토큰이 만료되었는지 확인합니다.

x-google-issuer, x-google-jwks_uri, x-google-audiences에 대한 자세한 내용은 OpenAPI 확장 프로그램을 참조하세요.

API 게이트웨이 API에 인증된 요청 수행

인증된 요청을 위해서는 API 구성에 지정한 서비스 계정으로 서명된 JWT를 호출 서비스에서 보내야 합니다. 호출 서비스는 다음을 수행해야 합니다.

  1. JWT를 만들고 이를 서비스 계정의 비공개 키로 서명합니다.
  2. 서명된 JWT를 API에 대한 요청에 포함하여 전송합니다.

다음 샘플 코드는 특정 언어에 대한 이 프로세스를 보여줍니다. 다른 언어로 인증된 요청을 수행하려면 jwt.io를 참조하여 지원되는 라이브러리 목록을 확인하세요.

  1. 호출 서비스에서 다음 함수를 추가하고 이를 다음 매개변수에 전달합니다.
    자바
    • saKeyfile: 서비스 계정의 비공개 키 파일에 대한 전체 경로입니다.
    • saEmail: 서비스 계정의 이메일 주소입니다.
    • audience: x-google-audiences 필드를 API 구성에 추가한 경우 audiencex-google-audiences에 지정한 값 중 하나로 설정합니다. 그 이외에는 audiencehttps://SERVICE_NAME으로 설정합니다. 여기에서 SERVICE_NAME은 API 게이트웨이 서비스 이름입니다.
    • expiryLength: JWT 만료 시간(초)입니다.
    Python
    • sa_keyfile: 서비스 계정의 비공개 키 파일에 대한 전체 경로입니다.
    • sa_email: 서비스 계정의 이메일 주소입니다.
    • audience: x-google-audiences 필드를 API 구성에 추가한 경우 audiencex-google-audiences에 지정한 값 중 하나로 설정합니다. 그 이외에는 audiencehttps://SERVICE_NAME으로 설정합니다. 여기에서 SERVICE_NAME은 API 게이트웨이 서비스 이름입니다.
    • expiry_length: JWT 만료 시간(초)입니다.
    Go
    • saKeyfile: 서비스 계정의 비공개 키 파일에 대한 전체 경로입니다.
    • saEmail: 서비스 계정의 이메일 주소입니다.
    • audience: x-google-audiences 필드를 API 구성에 추가한 경우 audiencex-google-audiences에 지정한 값 중 하나로 설정합니다. 그 이외에는 audiencehttps://SERVICE_NAME으로 설정합니다. 여기에서 SERVICE_NAME은 API 게이트웨이 서비스 이름입니다.
    • expiryLength: JWT 만료 시간(초)입니다.

    이 함수는 JWT를 만들고 비공개 키 파일을 사용하여 이에 서명한 다음, 서명된 JWT를 반환합니다.

    자바
    /**
     * 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: %v", err)
    	}
    	conf, err := google.JWTConfigFromJSON(sa)
    	if err != nil {
    		return "", fmt.Errorf("Could not parse service account JSON: %v", err)
    	}
    	block, _ := pem.Decode(conf.PrivateKey)
    	parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    	if err != nil {
    		return "", fmt.Errorf("private key parse error: %v", 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. 호출 서비스에서 다음 함수를 추가해서 API에 대한 요청의 Authorization: Bearer 헤더에 서명된 JWT를 보냅니다.
    자바
    /**
     * 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: %v", 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: %v", err)
    	}
    	defer response.Body.Close()
    	responseData, err := ioutil.ReadAll(response.Body)
    	if err != nil {
    		return "", fmt.Errorf("failed to parse HTTP response: %v", err)
    	}
    	return string(responseData), nil
    }
    

JWT를 사용하여 요청을 보낼 때는 보안상의 이유로 Authorization: Bearer 헤더에 인증 토큰을 포함하는 것이 좋습니다. 예를 들면 다음과 같습니다.

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

여기서 GATEWAY_URLTOKEN은 각각 배포된 게이트웨이 URL 및 인증 토큰을 포함하는 환경 변수입니다.

API에서 인증 결과 수신

API 게이트웨이는 수신하는 모든 헤더를 전달합니다. 하지만 API 구성에서 백엔드 주소가 x-google-backend로 지정된 경우 원래 Authorization 헤더를 재정의합니다.

API 게이트웨이는 X-Apigateway-Api-Userinfo의 인증 결과를 백엔드 API에 전송합니다. 원래 Authorization 헤더 대신 이 헤더를 사용하는 것이 좋습니다. 이 헤더는 base64url로 인코딩되며 JWT 페이로드를 포함합니다.

다음 단계