서비스 간 인증

사용자 인증 후 API와 다른 서비스 간의 상호 작용도 필요할 수 있습니다. 사용자는 클라이언트 애플리케이션에서 보내는 웹 로그인 프롬프트를 통해 사용자 인증 정보를 제출할 수 있지만, 서비스 간의 안전한 통신에는 다른 접근 방식이 필요합니다. 이 페이지는 서비스 간 인증 구현을 위한 권장 사항과 샘플 코드를 보여줍니다.

개요

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

API와 호출 서비스 사이의 인증 구현 절차는 다음과 같습니다.

  1. 호출 서비스에서 사용할 서비스 계정과 키를 만듭니다.
  2. Cloud Endpoints 서비스에 대한 OpenAPI 문서의 인증 지원을 추가합니다.
  3. 호출 서비스에 다음과 같은 코드를 추가합니다.

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

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

기본 요건

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

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

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

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

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

Google Cloud Console

  1. 서비스 계정을 만듭니다.

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

      서비스 계정 만들기 페이지로 이동

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

    3. 서비스 계정 이름 필드에 이름을 입력합니다.

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

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

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

    7. 완료를 클릭합니다.

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

  2. 서비스 계정 키 만들기

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

gcloud

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

  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 구성

ESP가 서명된 JWT의 클레임을 확인하려면 OpenAPI 문서에 보안 요구사항 객체보안 정의 객체가 있어야 합니다.

  1. OpenAPI 문서에 서비스 계정을 발급기관으로 추가합니다.

    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를 서비스 계정의 이메일 주소로 바꿉니다.
    • OpenAPI 문서가 여러 개의 보안 정의를 포함할 수는 있지만 각 정의의 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를 추가하지 않으면 ESP에는 https://SERVICE_NAME 형식으로 된 JWT의 "aud"(잠재고객) 클레임이 필요합니다. 여기에서 SERVICE_NAME은 플래그 --disable_jwt_audience_service_name_check가 사용되지 않은 한 OpenAPI 문서의 host 필드에 구성한 Endpoints 서비스의 이름입니다. 플래그가 사용되고 x-google-audiences가 지정되지 않으면 JWT aud 필드가 확인되지 않습니다.

  3. 선택사항으로 x-google-jwt-locationssecurityDefinitions 섹션에 추가합니다. 이 값을 사용하여 커스텀 JWT 위치를 정의할 수 있습니다. 기본 JWT 위치는 Authorization 헤더('Bearer'가 앞에 나옴), X-Goog-Iap-Jwt-Assertion 헤더 또는 access_token 쿼리 매개변수입니다. 참고:

    • x-google-jwt-locations를 지정하면 Endpoints는 모든 기본 위치를 무시합니다.
    • x-google-jwt-locations는 ESPv2에서만 지원됩니다.
  4. security 섹션을 파일의 최상위 수준(들여쓰거나 중첩하지 않음)에서 추가하여 전체 API에 적용하거나 메서드 수준에서 추가하여 특정 메서드에 적용합니다. API 및 메서드 수준 모두에 security 섹션을 사용하는 경우에는 메서드 수준 설정이 API 수준 설정보다 우선 적용됩니다.

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

      security:
        - service-1: []
        - service-2: []
      
  5. 업데이트된 OpenAPI 문서를 배포합니다. OPENAPI_DOC를 OpenAPI 문서의 이름으로 바꿉니다.

    gcloud endpoints services deploy OPENAPI_DOC

ESP는 요청을 API에 전달하기 전에 다음을 확인합니다.

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

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

Endpoints API에 인증된 요청 실행

인증된 요청을 위해서는 OpenAPI 문서 상 지정된 서비스 계정이 서명한 JWT를 호출 서비스에서 보내야 합니다. 호출 서비스는 다음을 수행해야 합니다.

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

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

  1. 호출 서비스에서 다음 함수를 추가하고 이를 다음 매개변수에 전달합니다.
    자바
    • saKeyfile: 서비스 계정의 비공개 키 파일에 대한 전체 경로입니다.
    • saEmail: 서비스 계정의 이메일 주소입니다.
    • audience: x-google-audiences 필드를 OpenAPI 문서에 추가한 경우 audiencex-google-audiences에 지정한 값 중 하나로 설정합니다. 그 이외에는 audiencehttps://SERVICE_NAME으로 설정합니다. 여기에서 SERVICE_NAME은 Endpoints 서비스 이름입니다.
    • expiryLength: JWT 만료 시간(초)입니다.
    Python
    • sa_keyfile: 서비스 계정의 비공개 키 파일에 대한 전체 경로입니다.
    • sa_email: 서비스 계정의 이메일 주소입니다.
    • audience: x-google-audiences 필드를 OpenAPI 문서에 추가한 경우 audiencex-google-audiences에 지정한 값 중 하나로 설정합니다. 그 이외에는 audiencehttps://SERVICE_NAME으로 설정합니다. 여기에서 SERVICE_NAME은 Endpoints 서비스 이름입니다.
    • expiry_length: JWT 만료 시간(초)입니다.
    Go
    • saKeyfile: 서비스 계정의 비공개 키 파일에 대한 전체 경로입니다.
    • saEmail: 서비스 계정의 이메일 주소입니다.
    • audience: x-google-audiences 필드를 OpenAPI 문서에 추가한 경우 audiencex-google-audiences에 지정한 값 중 하나로 설정합니다. 그 이외에는 audiencehttps://SERVICE_NAME으로 설정합니다. 여기에서 SERVICE_NAME은 Endpoints 서비스 이름입니다.
    • 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: %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. 호출 서비스에서 다음 함수를 추가해서 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: %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}" \
  "${ENDPOINTS_HOST}/echo"

여기서 ENDPOINTS_HOSTTOKEN은 각각 API 호스트 이름과 인증 토큰을 포함하는 환경 변수입니다.

API에서 인증 결과 수신

ESP는 수신하는 모든 헤더를 전달합니다. 하지만 OpenAPI 사양의 x-google-backend 또는 gRPC 서비스 구성의 BackendRule에서 백엔드 주소를 지정할 때 ESP는 원래 Authorization 헤더를 재정의합니다.

ESP는 X-Endpoint-API-UserInfo의 인증 결과를 백엔드 API에 전송합니다. 원래 Authorization 헤더 대신 이 헤더를 사용하는 것이 좋습니다. 이 헤더는 base64url이 JSON 객체를 인코딩하는 문자열입니다. JSON 객체 형식은 ESPv2와 ESP 간에 다릅니다. ESPv2의 경우 JSON 객체는 정확히 원래 JWT 페이로드입니다. ESP의 경우 JSON 객체는 서로 다른 필드 이름을 사용하고 원래 JWT 페이로드를 claims 필드에 넣습니다. 형식에 대한 자세한 내용은 백엔드 서비스에서 JWT 처리를 참조하세요.

다음 단계