はじめに
テスト環境
- IDE
- UE:5.3.2
- Visual Studio 2022:17.11.5
- プラグイン等
- TsubasamusuUnrealLibrary:1.10.0
手順
サービスアカウントの作成
手順1
以下の記事などを参考にして Google アカウントの作成や無料トライアルへの登録、プロジェクトの作成などの Googe Cloud の初期設定を行う。
- Google Cloud を初めて利用する方は、ここから始めましょう
- 【超初心者向け】Google Cloudを始めるための事前準備
- 基礎から学ぶGCP 第1回 〜GCPを使う準備をする〜
- Google Cloud(旧GCP)無料で使ってみた!クラウド初心者もかんたんに開設、始め方大解説(前編:説明編)
- 【Google Cloud】【最短10分】無料ではじめるGoogle Cloud入門
- 【初心者向け】Google Cloudのアカウント作成手順を分かりやすく解説!
手順2
Google Cloud にアクセスして「IAM と管理>サービスアカウント」に行き、「サービスアカウントを作成」を押す。
手順3
「サービスアカウント名」と「サービスアカウント ID」を入力して「作成して続行」を押す。
手順4
必要に応じて「ロールを選択」を押してロールを選択し、「続行」を押す。
例:Cloud Storage のオブジェクトを読み取る場合は「Cloud Storage > Storage オブジェクト閲覧者」など
手順5
必要に応じて「サービスアカウントユーザーロール」と「サービスアカウント管理者ロール」を設定して「完了」を押す。
秘密鍵のダウンロード
手順6
作成したサービスアカウントを選択して「キー>鍵を追加>新しい鍵を作成」を押す。
手順7
「キーのタイプ」で「JSON」を選択して右下の「作成」を押し、JSON ファイルをダウンロードする。
API の使用(Cloud Translation API の場合)
手順8
Google Cloud に戻り、使用したい API の「有効にする」を押す。
手順9
UE にて、Google Cloud の API を叩く際に必要なアクセストークンを取得する以下のような関数を作成する。
(以下は TsubasamusuUnrealLibrary(v1.10.0) のコード)
UTsubasamusuStringConvertLibrary
#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);
};
#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
#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);
};
#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();
}
手順10
API ドキュメントを確認しながら API を叩く関数を作成する。
ヘッダーの「Authorization」に Google Cloud 用のアクセストークンを渡してあげるのがポイント。
(以下は Cloud Translation API の例)
#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 ドキュメントに書かれている必要なスコープを渡してあげるのがポイント。