プレイヤー検証をゲームセッションに統合する
注釈:本資料はAI技術を用いて翻訳されています。
概要
AccelByte Gaming Services (AGS) Session には、専用サーバー (DS) によって維持されるゲームセッションにプレイヤーが入る際のセキュリティレベルを強化するゲームセッションプレイヤー検証機能が含まれています。DS は、接続を試みるゲームクライアントが有効なユーザーとして認証され、セッションのメンバーであることを確認する必要があります。プロセスは次のように動作します。
- セッションサービスがシークレットキーを生成し、それを所持すべきプレイヤーに返します。
- ゲームクライアントが DS への接続を試みると、サーバーはプレイヤーのユーザー ID に基づいてシークレットキーを検証します。
- 検証が完了すると、プレイヤーのゲームクライアントは DS に正常に接続できます。
この記事では、ゲームクライアントと DS の両側での AGS Session のプレイヤー検証機能の実装について説明します。
前提条件
この記事の手順を完了するには、以下が必要です。
- AGS Admin Portal へのアクセス。
- マッチの設定に関する知識。
- ゲームセッションの設定と参加に関する知識。
フローとコードスニペット
セッションプレイヤー検証の実装プロセスを説明しやすくするために、DS とゲームクライアントの 2 つの部分で構成されていると考えることができます。DS 側では、この記事ではサーバーがシークレットを生成する方法を説明します。ゲームクライアント側では、この記事ではシークレットを取得する方法を説明します。これは、ゲームセッションに参加するか、マッチメイキングを通じてセッションを作成することで発生する可能性があり、ロビーで接続の問題が発生した場合にシークレットを取得する方法も含まれます。
ゲームサーバー
マッチメイキングが正常にマッチを見つけると、ゲームセッションが作成され、専用サーバーが割り当てられます。その後、ゲームサーバーは、AGS Session からの DS Hub WebSocket 接続を介してシークレットの通知を受け取ります。
- Unreal Engine
- Unity
void AMyActor::ConnectToDSHub()
{
...
const AccelByte::GameServerApi::FOnV2SessionSecretUpdateNotification
OnDSHubSessionSecreteUpdateNotificationDelegate =
AccelByte::GameServerApi::FOnV2SessionSecretUpdateNotification::CreateUObject(this, &AMyActor::OnDSHubSessionSecreteUpdateNotification);
ServerApiClient->ServerDSHub.SetOnV2SessionSecretUpdateNotification(OnDSHubSessionSecreteUpdateNotificationDelegate);
...
ServerApiClient->ServerDSHub.Connect(ServerName);
}
void AMyActor::OnDSHubSessionSecreteUpdateNotification(const FAccelByteModelsSessionSecretUpdateNotification& Notification)
{
UE_LOG(LogTemp, Log, TEXT("DS SessionSecreteUpdate, Secret:%s"), *Notification.Secret);
}
using AccelByte.Api;
using AccelByte.Models;
using UnityEngine;
public class DSController : MonoBehaviour
{
//server Id format is ds_ and followed by uuidv4
//it is set by dedicated server
string serverId = "ds_8f92300b-9791-475b-97ef-db14b445e415";
string sessionSecret = "";
void ConnectToDSHub()
{
// ...
AccelByteSDK.GetServerRegistry().GetApi().GetDsHub().SessionServerSecretReceived += (notification) =>
{
if (notification.IsError)
{
Debug.LogWarning($"Failed to receive SessionSecretUpdate");
return;
}
Debug.Log($"DS SessionSecreteUpdate, Secret:{notification.Value.secret}");
// session secret will be used later for session validation
sessionSecret = notification.Value.secret;
};
// ...
AccelByteSDK.GetServerRegistry().GetApi().GetDsHub().Connect(ServerId);
}
}
ゲームクライアント
ゲームクライアントは、ゲームセッションに参加するか、マッチメイキングを通じてセッションを作成することでシークレットを取得できます。また、ロビーで接続の問題が発生した場合の対処方法も知ることができます。
マッチメイキングを開始するか、ゲームセッションに参加してセッションを正常に取得した後、ゲームクライアントは、ロビー WebSocket 接続を介してシークレットの通知を受け取ります。
- Unreal Engine
- Unity
void AMyActor::ConnectToLobby()
{
...
const AccelByte::Api::Lobby::FV2SessionJoinedSecretNotif V2SessionJoinedSecretNotifDelegate =
AccelByte::Api::Lobby::FV2SessionJoinedSecretNotif::CreateUObject(this, &AMyActor::OnV2SessionJoinedSecretNotif); ApiClient->Lobby.SetV2SessionJoinedSecretNotifDelegate(V2SessionJoinedSecretNotifDelegate);
...
ApiClient->Lobby.Connect();
}
void AMyActor::OnV2SessionJoinedSecretNotif(const FAccelByteModelsV2SessionJoinedSecret& Data)
{
UE_LOG(LogTemp, Log, TEXT("Client OnV2SessionJoinedSecretNotif, Secret=%s"), *Data.Secret);
}
using AccelByte.Api;
using AccelByte.Models;
using UnityEngine;
public class DSController : MonoBehaviour
{
void ConnectToLobby()
{
// ...
AccelByteSDK.GetClientRegistry().GetLobby().SessionSecret += (notif) =>
{
Debug.Log($"Client Session Secret, Secret={notif.Value.secret}");
};
// ...
AccelByteSDK.GetClientRegistry().GetLobby().Connect();
}
}
マッチメイキングの開始
ゲームクライアントは、マッチメイキングを開始することで通知を受け取ることができます。
- Unreal Engine
- Unity
void AMyActor::CreateMatchTicket(const FString& SessionId)
{
const FString MatchPoolName = PoolName;
FAccelByteModelsV2MatchTicketOptionalParams Option;
Option.Latencies = ApiClient->Qos.GetCachedLatencies();
FJsonObject AttributesJsonObj{};
AttributesJsonObj.SetStringField("server_name", ServerName);
FString JsonString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonString);
TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>(AttributesJsonObj);
FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);
// Convert to JSON Object Wrapper
FJsonObjectWrapper Attributes;
FAccelByteJsonConverter::JsonObjectStringToUStruct(JsonString, &Attributes);
Option.Attributes = Attributes;
Option.SessionId = SessionId;
ApiClient->MatchmakingV2.CreateMatchTicket(MatchPoolName,
AccelByte::THandler<FAccelByteModelsV2MatchmakingCreateTicketResponse>::CreateUObject(this, &AMyActor::OnCreateMatchTicketSuccess),
AccelByte::FCreateMatchmakingTicketErrorHandler::CreateUObject(this, &AMyActor::OnCreateMatchTicketFail));
}
void AMyActor::OnCreateMatchTicketSuccess(const FAccelByteModelsV2MatchmakingCreateTicketResponse& Data)
{
UE_LOG(LogTemp, Log, TEXT("Client OnCreateMatchTicketSuccess "));
}
void AMyActor::OnCreateMatchTicketFail(int32 ErrorCode, const FString& ErrorMessage, const FErrorCreateMatchmakingTicketV2& CreateTicketErrorInfo)
{
UE_LOG(LogTemp, Log, TEXT("Client OnCreateMatchTicketFail, Error code: %d\nError message:%s"), ErrorCode, *ErrorMessage);
}
using AccelByte.Api;
using AccelByte.Models;
using UnityEngine;
public class ClientController : MonoBehaviour
{
public void CreateMatchTicket(string sessionId, string matchPoolName)
{
Dictionary<string, int> latencies = new Dictionary<string, int>();
AccelByteSDK.GetClientRegistry().GetApi().GetQos().GetAllActiveServerLatencies(result =>
{
if (!result.IsError)
{
latencies = result.Value;
}
else
{
Debug.LogWarning("Failed to fetch active server latencies");
}
});
var attributes = new Dictionary<string, object>()
{
{ "server_name", "my_server_123" }
};
var additionalParam = new MatchmakingV2CreateTicketRequestOptionalParams()
{
latencies = latencies,
attributes = attributes
};
AccelByteSDK.GetClientRegistry().GetApi().GetMatchmakingV2().CreateMatchTicket(matchPoolName, additionalParam, result =>
{
if (result.IsError)
{
Debug.LogWarning($"Client OnCreateMatchTicketFail, Error code: {result.Error.Code}\nError message:{result.Error.Message}");
}
else
{
Debug.Log("Client OnCreateMatchTicketSuccess");
}
});
}
}
ゲームセッションへの参加
ゲームセッションがすでに DS によって作成されている場合、ゲームセッションに入るもう 1 つのオプションは、ゲームセッションに直接参加することです。
- Unreal Engine
- Unity
void AMyActor::JoinGameSession(const FString& SessionId)
{
ApiClient->Session.JoinGameSession(
SessionId,
AccelByte::THandler<FAccelByteModelsV2GameSession>::CreateUObject(this, &AMyActor::OnJoinGameSessionSuccess),
AccelByte::FErrorHandler::CreateUObject(this, &AMyActor::OnJoinGameSessionFail));
}
void AMyActor::OnJoinGameSessionSuccess(const FAccelByteModelsV2GameSession& Result)
{
UE_LOG(LogTemp, Log, TEXT("Join Game Session Success"));
}
void AMyActor::OnJoinGameSessionFail(int32 ErrorCode, const FString& ErrorMessage)
{
UE_LOG(LogTemp, Log, TEXT("Join Game Session Fail, Error code: %d\nError message:%s"), ErrorCode, *ErrorMessage);
}
using AccelByte.Api;
using AccelByte.Models;
using UnityEngine;
public class CLientController : MonoBehaviour
{
public void JoinGameSession(string sessionId)
{
AccelByteSDK.GetClientRegistry().GetApi().GetSession().JoinGameSession(sessionId, result =>
{
if (result.IsError)
{
Debug.LogWarning($"Join Game Session Fail, Error code: {result.Error.Code}\nError message: {result.Error.Message}");
}
else
{
Debug.Log("Join Game Session Success");
}
});
}
}
サーバーへの再接続
ゲームクライアントがサーバーから切断された場合、または以前のサービスから受信したシークレットを持っていない場合、ゲームクライアントは、REST API レスポンスを使用して関数を介して直接呼び出すことで、AGS Session からシークレットを取得できます。
- Unreal Engine
- Unity
void AMyActor::GetGameSessions()
{
ApiClient->SessionBrowser.GetGameSessions(
EAccelByteSessionType::dedicated,
GameMode,
AccelByte::THandler<FAccelByteModelsSessionBrowserGetResult>::CreateUObject(this, &AMyActor::OnGetGameSessionsSuccess),
AccelByte::FErrorHandler::CreateUObject(this, &AMyActor::OnGetGameSessionsFail) );
}
void AMyActor::OnGetGameSessionsSuccess(const FAccelByteModelsSessionBrowserGetResult& Result)
{
UE_LOG(LogTemp, Log, TEXT("OnGetGameSessionsSuccess "));
}
void AMyActor::OnGetGameSessionsFail(int32 ErrorCode, const FString& ErrorMessage)
{
UE_LOG(LogTemp, Log, TEXT("OnGetGameSessionsFail, Error code: %d\nError message:%s"), ErrorCode, *ErrorMessage);
}
public void GetGameSessions()
{
var session = AccelByteSDK.GetClientRegistry().GetApi().GetSession();
var attributeRequest = new Dictionary<string, object>()
{
{ "type", "dedicated" },
};
session.QueryGameSession(attributeRequest, result =>
{
if (result.IsError)
{
// Do something if QueryGameSession has an error
Debug.Log($"Error QueryGameSession, Error Code: {result.Error.Code} Error Message: {result.Error.Message}");
return;
}
// Do something if QueryGameSession succeeds
});
}
プレイヤー検証
ゲームクライアントは DS への接続を試み、DS はプレイヤーのユーザー ID に基づいてシークレットキーを検証します。検証が完了すると、ゲームクライアントは DS に正常に接続できます。
TOTP の生成
ゲームクライアントは、セッションシークレットによって SDK に時間ベースの OTP (TOTP) をリクエストします。逆に、DS も同じシークレットを使用してゲームクライアントから送信された TOTP 値をチェックします。両側の 2 つの値を比較するには、次の関数を使用できます。
- Unreal Engine
- Unity
void AMyActor::GenerateTOTP(const FString& SecretKey)
{
const FString Value = FAccelByteUtilities::GenerateTOTP(SecretKey);
UE_LOG(LogTemp, Log, TEXT("GenerateTOTP value = %s"), *Value);
}
using AccelByte.Api;
using AccelByte.Core;
using AccelByte.Utils;
using UnityEngine;
public class ClientController : MonoBehaviour
{
public void GenerateTOTP(string secretKey)
{
var totpUtils = new TotpUtils();
string value = totpUtils.GenerateTotp(secretKey);
Debug.Log($"GenerateTOTP value = {value}");
}
}
TOTP を使用したクライアントトラベル
- Unreal Engine
- Unity
void AMyActor::ClientTravel(const FString& TOTP)
{
TravelUrl += FString::Printf(TEXT("?totp=%s?userid=%s"), *TOTP, *ApiClient->CredentialsRef->GetUserId());
auto PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
PC->ClientTravel(TravelUrl, TRAVEL_Absolute);
}
using AccelByte.Core;
using AccelByte.Utils;
using UnityEngine;
public class ClientController : MonoBehaviour
{
public void ConnectToDS(string serverIp, ushort port, string totp)
{
// Get the local player's User ID from the AccelByte SDK
string userId = AccelByteSDK.GetClientRegistry().GetApi().session.UserId;
// 1. Format your custom data as a query string
string queryStringPayload = $"?totp={totp}&userId={userId}";
// 2. Convert the query string to a byte array
byte[] payloadBytes = Encoding.UTF8.GetBytes(queryStringPayload);
// 3. Set the payload in the NetworkConfig's ConnectionData
NetworkManager.Singleton.NetworkConfig.ConnectionData = payloadBytes;
// 4. Set connection details on the transport
var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();
transport.SetConnectionData(serverIp, port);
// 5. Start the client. The payload will be sent during the handshake.
NetworkManager.Singleton.StartClient();
}
}
DS の有効性チェック
- Unreal Engine
- Unity
void AAccelByteGameMode::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
{
UE_LOG(LogAccelByteDSTest, Warning, TEXT("Option: %s, Address %s"), *Options, *Address);
TArray<FString> Out;
Options.ParseIntoArray(Out, TEXT("?"), true);
FString TOTP{};
FString UserId{};
for (auto Option : Out)
{
FString Param, Value;
Option.Split(TEXT("="), &Param, &Value);
if (Param == TEXT("totp"))
{
TOTP = Value;
}
if (Param == TEXT("userid"))
{
UserId = Value;
}
}
bool Valid = FAccelByteUtilities::ValidateTOTP(Secret, TOTP, UserId);
UE_LOG(LogAccelByteDSTest, Log, TEXT("User Validity = %s"), Valid ? TEXT("Valid") : TEXT("Invalid"));
if (!Valid)
{
return;
}
// Forward to PreLogin
Super::PreLogin(Options, Address, UniqueId, ErrorMessage);
}
using AccelByte.Core;
using AccelByte.Utils;
using UnityEngine;
public class DSController : MonoBehaviour
{
void Start()
{
var networkManager = NetworkManager.Singleton;
networkManager.NetworkConfig.ConnectionApproval = true;
networkManager.ConnectionApprovalCallback += ApprovalCheck;
networkManager.StartServer();
}
private void ApprovalCheck(
NetworkManager.ConnectionApprovalRequest request,
NetworkManager.ConnectionApprovalResponse response)
{
// --- Fetch the Payload ---
byte[] payloadBytes = request.Payload;
string queryString = Encoding.UTF8.GetString(payloadBytes);
Debug.Log($"Server: Received connection request from client {request.ClientNetworkId}.");
Debug.Log($"Server: Raw query string payload: {queryString}");
// --- Parse the Query String ---
// You'll need a way to parse the query string.
// One way is to manually split it, or use a utility.
// For example, a simple manual parse:
string totp = "";
string userId = "";
// Remove the leading '?' if present
if (queryString.StartsWith("?"))
{
queryString = queryString.Substring(1);
}
string[] pairs = queryString.Split('&');
foreach (string pair in pairs)
{
string[] keyValue = pair.Split('=');
if (keyValue.Length == 2)
{
string key = keyValue[0];
string value = keyValue[1]; // You might want to HttpUtility.UrlDecode this value
if (key == "totp")
{
totp = value;
}
else if (key == "userId")
{
userId = value;
}
}
}
Debug.Log($"Server: Parsed TOTP: {totp}, UserId: {userId}");
// --- Your Validation Logic ---
var totpUtils = new TotpUtils();
// The 'Secret' variable is the server secret received from the DSHub's
// 'SessionServerSecretReceived' event when the DS first connected.
// It should be stored securely by the server.
bool isValid = totpUtils.ValidateTOTP(sessionSecret, totp, userId);
Debug.Log($"User Validity = {(isValid ? "Valid" : "Invalid")}");
response.Approved = isValid;
response.CreatePlayerObject = true;
if (isValid)
{
Debug.Log($"Server: Approved connection for client {request.ClientNetworkId}.");
}
else
{
response.Reason = "Validation failed.";
Debug.LogWarning($"Server: Denied connection for client {request.ClientNetworkId}. Reason: {response.Reason}");
}
}
}