1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Unreal Engine (UE)Advent Calendar 2024

Day 24

【GCP/UE】UE から Google Cloud の API を叩く基本的な流れ

Last updated at Posted at 2024-12-23

はじめに

テスト環境

  • IDE
    • UE:5.3.2
    • Visual Studio 2022:17.11.5
  • プラグイン等
    • TsubasamusuUnrealLibrary:1.10.0

手順

サービスアカウントの作成

手順1

以下の記事などを参考にして Google アカウントの作成や無料トライアルへの登録、プロジェクトの作成などの Googe Cloud の初期設定を行う。

手順2

Google Cloud にアクセスして「IAM と管理>サービスアカウント」に行き、「サービスアカウントを作成」を押す。

TSUBASAMUSU.png

手順3

「サービスアカウント名」と「サービスアカウント ID」を入力して「作成して続行」を押す。

TSUBASAMUSU.png

手順4

必要に応じて「ロールを選択」を押してロールを選択し、「続行」を押す。

例:Cloud Storage のオブジェクトを読み取る場合は「Cloud Storage > Storage オブジェクト閲覧者」など

TSUBASAMUSU.png

手順5

必要に応じて「サービスアカウントユーザーロール」と「サービスアカウント管理者ロール」を設定して「完了」を押す。

TSUBASAMUSU.png

秘密鍵のダウンロード

手順6

作成したサービスアカウントを選択して「キー>鍵を追加>新しい鍵を作成」を押す。

TSUBASAMUSU.png

手順7

「キーのタイプ」で「JSON」を選択して右下の「作成」を押し、JSON ファイルをダウンロードする。

TSUBASAMUSU.png

API の使用(Cloud Translation API の場合)

手順8

Google Cloud に戻り、使用したい API の「有効にする」を押す。

TSUBASAMUSU.png

手順9

UE にて、Google Cloud の API を叩く際に必要なアクセストークンを取得する以下のような関数を作成する。
(以下は TsubasamusuUnrealLibrary(v1.10.0) のコード)

UTsubasamusuStringConvertLibrary
TsubasamusuStringConvertLibrary.h
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "TsubasamusuStringConvertLibrary.generated.h"

UCLASS()
class TSUBASAMUSUUNREALLIBRARY_API UTsubasamusuStringConvertLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintPure, Category = "TSUBASAMUSU|Convert|String")
	static FString ConvertToBase64(const FString& String);

	UFUNCTION(BlueprintCallable, Category = "TSUBASAMUSU|Convert|String")
	static TArray<uint8> ConvertToUtf8(const FString& String);
	
	static TArray<char> ConvertToCharArray(const FString& String);
};
TsubasamusuStringConvertLibrary.cpp
#include "Convert/TsubasamusuStringConvertLibrary.h"

FString UTsubasamusuStringConvertLibrary::ConvertToBase64(const FString& String)
{
    FTCHARToUTF8 Utf8String(*String);

    return FBase64::Encode((const uint8*)Utf8String.Get(), Utf8String.Length());
}

TArray<uint8> UTsubasamusuStringConvertLibrary::ConvertToUtf8(const FString& String)
{
    FTCHARToUTF8 Utf8(*String);

    TArray<uint8> StringData;

    StringData.Append((uint8*)Utf8.Get(), Utf8.Length());

    return StringData;
}

TArray<char> UTsubasamusuStringConvertLibrary::ConvertToCharArray(const FString& String)
{
    FTCHARToUTF8 UTF8String(*String);

    TArray<char> CharArray;

    CharArray.Reserve(UTF8String.Length());

    CharArray.Append(UTF8String.Get(), UTF8String.Length());

    return CharArray;
}
UAsyncActionGetGoogleAccessToken
AsyncActionGetGoogleAccessToken.h
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AsyncActionGetGoogleAccessToken.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSucceededToGetGoogleAccessToken, const FString&, AccessToken, int64, ExpirationUnixTime);

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnFailedToGetGoogleAccessToken);

UCLASS()
class TSUBASAMUSUUNREALLIBRARY_API UAsyncActionGetGoogleAccessToken : public UBlueprintAsyncActionBase
{
	GENERATED_BODY()

public:
	UPROPERTY(BlueprintAssignable)
	FOnSucceededToGetGoogleAccessToken Succeeded;

	UPROPERTY(BlueprintAssignable)
	FOnFailedToGetGoogleAccessToken Failed;

	UFUNCTION(BlueprintCallable, Category = "TSUBASAMUSU|Google", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject", AutoCreateRefTerm = "Scopes"))
	static UAsyncActionGetGoogleAccessToken* AsyncGetGoogleAccessToken(UObject* WorldContextObject, const FString& PrivateKey, const FString& ServiceAccountMailAddress, const TArray<FString>& Scopes);

	void Activate() override;

private:
	FString PrivateKey;

	FString ServiceAccountMailAddress;

	TArray<FString> Scopes;

	int64 ExpirationUnixTime;

	FString GetGoogleAccessTokenHeader();

	FString GetGoogleAccessTokenPayload();

	FString GetGoogleCloudJwt();

	FString GetGoogleCloudJsonContent();

	void OnSucceeded(const FString& AccessToken);

	void OnFailed(const FString& TriedThing);
};
AsyncActionGetGoogleAccessToken.cpp
#include "Google/AsyncActionGetGoogleAccessToken.h"
#include "Interfaces/IHttpRequest.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"

#define UI UI_ST
THIRD_PARTY_INCLUDES_START
#include <openssl/sha.h>
#include <openssl/bio.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
THIRD_PARTY_INCLUDES_END
#undef UI

UAsyncActionGetGoogleAccessToken* UAsyncActionGetGoogleAccessToken::AsyncGetGoogleAccessToken(UObject* WorldContextObject, const FString& PrivateKey, const FString& ServiceAccountMailAddress, const TArray<FString>& Scopes)
{
	UAsyncActionGetGoogleAccessToken* Action = NewObject<UAsyncActionGetGoogleAccessToken>();

	Action->PrivateKey = PrivateKey;
	Action->ServiceAccountMailAddress = ServiceAccountMailAddress;
	Action->Scopes = Scopes;

	Action->RegisterWithGameInstance(WorldContextObject);

	return Action;
}

void UAsyncActionGetGoogleAccessToken::Activate()
{
	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HttpRequest = FHttpModule::Get().CreateRequest();

	FString Url = TEXT("https://oauth2.googleapis.com/token");

	HttpRequest->SetURL(Url);
	HttpRequest->SetVerb(TEXT("POST"));
	HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
	HttpRequest->SetContentAsString(GetGoogleCloudJsonContent());

	HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequestPtr, FHttpResponsePtr HttpResponsePtr, bool bSucceeded)
		{
			if (!bSucceeded)
			{
				OnFailed(TEXT("send a HTTP request"));

				return;
			}

			if (!HttpResponsePtr.IsValid())
			{
				OnFailed(TEXT("get a FHttpResponsePtr"));

				return;
			}

			FString JsonResponse = HttpResponsePtr->GetContentAsString();

			TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(JsonResponse);

			TSharedPtr<FJsonObject> JsonObject;

			if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid())
			{
				UTsubasamusuLogLibrary::LogError(TEXT("The JSON response is \"") + JsonResponse + TEXT("\"."));

				OnFailed(TEXT("deserialize the JSON response"));

				return;
			}

			FString AccessToken;

			if (JsonObject->TryGetStringField(TEXT("access_token"), AccessToken) || JsonObject->TryGetStringField(TEXT("id_token"), AccessToken))
			{
				OnSucceeded(AccessToken);

				return;
			}

			FString ErrorMessage, ErrorDescription;

			if (JsonObject->TryGetStringField(TEXT("error"), ErrorMessage) && JsonObject->TryGetStringField(TEXT("error_description"), ErrorDescription))
			{
				UTsubasamusuLogLibrary::LogError(TEXT("The error is \"") + ErrorMessage + TEXT("\"."));
				UTsubasamusuLogLibrary::LogError(TEXT("The error description is \"") + ErrorDescription + TEXT("\"."));

				OnFailed(TEXT("get an access token field from the JSON response"));

				return;
			}

			UTsubasamusuLogLibrary::LogError(TEXT("The JSON response is \"") + JsonResponse + TEXT("\"."));

			OnFailed(TEXT("get any field from the JSON response"));
		});

	if (!HttpRequest->ProcessRequest()) OnFailed(TEXT("process a HTTP request"));
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleAccessTokenHeader()
{
	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetStringField(TEXT("alg"), TEXT("RS256"));
	JsonObject->SetStringField(TEXT("typ"), TEXT("JWT"));

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter)) return JsonString;

	OnFailed(TEXT("create a JSON header"));

	return TEXT("");
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleAccessTokenPayload()
{
	int64 IssuedUnixTime = FDateTime::UtcNow().ToUnixTimestamp();
	ExpirationUnixTime = IssuedUnixTime + 3600;

	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetStringField(TEXT("iss"), ServiceAccountMailAddress);
	JsonObject->SetStringField(TEXT("scope"), FString::Join(Scopes, TEXT(" ")));
	JsonObject->SetStringField(TEXT("aud"), TEXT("https://oauth2.googleapis.com/token"));
	JsonObject->SetNumberField(TEXT("exp"), ExpirationUnixTime);
	JsonObject->SetNumberField(TEXT("iat"), IssuedUnixTime);

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter)) return JsonString;

	OnFailed(TEXT("create a JSON payload"));

	return TEXT("");
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleCloudJwt()
{
	TArray<char> PrivateKeyCharArray = UTsubasamusuStringConvertLibrary::ConvertToCharArray(PrivateKey.Replace(TEXT("\\n"), TEXT("\n")));

	BIO* Bio = BIO_new_mem_buf((void*)PrivateKeyCharArray.GetData(), -1);

	if (!Bio)
	{
		OnFailed(TEXT("create a BIO"));

		return TEXT("");
	}

	RSA* Rsa = PEM_read_bio_RSAPrivateKey(Bio, nullptr, nullptr, nullptr);

	BIO_free(Bio);

	if (!Rsa)
	{
		OnFailed(TEXT("create a RSA"));

		return TEXT("");
	}

	FString HeaderBase64 = UTsubasamusuStringConvertLibrary::ConvertToBase64(GetGoogleAccessTokenHeader());
	FString PayloadBase64 = UTsubasamusuStringConvertLibrary::ConvertToBase64(GetGoogleAccessTokenPayload());

	FString Message = HeaderBase64 + TEXT(".") + PayloadBase64;

	TArray<uint8> MessageDigest = UTsubasamusuStringConvertLibrary::ConvertToUtf8(Message);

	uint8 Digest[SHA256_DIGEST_LENGTH];

	SHA256(MessageDigest.GetData(), MessageDigest.Num(), Digest);

	uint8 Signature[256];

	unsigned int SignatureLen;

	if (RSA_sign(NID_sha256, Digest, SHA256_DIGEST_LENGTH, Signature, &SignatureLen, Rsa) != 1)
	{
		RSA_free(Rsa);

		OnFailed(TEXT("sign the RSA"));

		return TEXT("");
	}

	RSA_free(Rsa);

	return Message + TEXT(".") + FBase64::Encode(Signature, SignatureLen);
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleCloudJsonContent()
{
	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetStringField(TEXT("grant_type"), TEXT("urn:ietf:params:oauth:grant-type:jwt-bearer"));
	JsonObject->SetStringField(TEXT("assertion"), GetGoogleCloudJwt());

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter)) return JsonString;

	OnFailed(TEXT("create a JSON content"));

	return TEXT("");
}

void UAsyncActionGetGoogleAccessToken::OnSucceeded(const FString& AccessToken)
{
	Succeeded.Broadcast(AccessToken, ExpirationUnixTime);

	SetReadyToDestroy();
}

void UAsyncActionGetGoogleAccessToken::OnFailed(const FString& TriedThing)
{
	UTsubasamusuLogLibrary::LogError(TEXT("Failed to ") + TriedThing + TEXT(" to get an access token for Google Cloud."));

	Failed.Broadcast();

	SetReadyToDestroy();
}
TSUBASAMUSU.png

手順10

API ドキュメントを確認しながら API を叩く関数を作成する。
ヘッダーの「Authorization」に Google Cloud 用のアクセストークンを渡してあげるのがポイント。
(以下は Cloud Translation API の例)

SampleBlueprintFunctionLibrary.cpp
#include "SampleBlueprintFunctionLibrary.h"
#include "Interfaces/IHttpRequest.h"
#include "HttpModule.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Interfaces/IHttpResponse.h"

void USampleBlueprintFunctionLibrary::Hoge(const FString& GoogleCloudAccessToken)
{
	TArray<TSharedPtr<FJsonValue>> JsonValues;

	JsonValues.Add(MakeShared<FJsonValueString>(TEXT("Hello world")));
	JsonValues.Add(MakeShared<FJsonValueString>(TEXT("My name is Tsubasamusu.")));

	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetArrayField(TEXT("q"), JsonValues);
	JsonObject->SetStringField(TEXT("target"), TEXT("de"));

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HttpRequest = FHttpModule::Get().CreateRequest();

	FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter);

	HttpRequest->SetURL(TEXT("https://translation.googleapis.com/language/translate/v2"));
	HttpRequest->SetVerb(TEXT("POST"));
	HttpRequest->SetHeader(TEXT("Content-type"), TEXT("application/json"));
	HttpRequest->SetHeader(TEXT("Authorization"), TEXT("Bearer ") + GoogleCloudAccessToken);//アクセストークンを渡す
	HttpRequest->SetContentAsString(JsonString);

	HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequestPtr, FHttpResponsePtr HttpResponsePtr, bool bSucceeded)
		{
			UKismetSystemLibrary::PrintString(nullptr, HttpResponsePtr->GetContentAsString());
		});

	HttpRequest->ProcessRequest();
}

手順11

実際に API を叩いて動作を確認する。
アクセストークンを発行する際に、API ドキュメントに書かれている必要なスコープを渡してあげるのがポイント。

TSUBASAMUSU.png

最後に

参考記事

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?