跳转到主要内容

会话保持及重复登录检测

定期向服务器发送会话保持信号,并确认是否在其他设备上重复登录的API。

示例代码说明

本文档的示例代码仅供参考。示例展示了使用FTimerHandle的基于定时器的实现,但请根据项目的架构和编码规范自由修改使用。

URL确认

此API使用 service-account.playnanoo.com 域名。

API信息

  • URL: https://service-account.playnanoo.com/api/v20240401/alive
  • Method: PUT
  • 需要认证: 是

请求参数

参数类型必填说明
platformstring必填平台 (例如: "aos", "ios")
device_idstring必填设备唯一ID
device_modelstring必填设备型号
device_osstring必填设备OS
device_languagestring必填设备语言 (例如: "KO", "EN")
DeviceInfo继承

此API的Req类继承自DeviceInfo。DeviceInfo的所有属性将自动包含。

响应数据

正常响应

字段类型说明
Statusstring状态 (正常: "success")

错误响应

错误代码说明
30006DuplicatedDeviceException - 已在其他设备上认证

Unreal C++实现

CheckAlive 类

// CheckAlive.h
#pragma once

#include "CoreMinimal.h"
#include "Http.h"
#include "Json.h"
#include "CheckAlive.generated.h"

USTRUCT(BlueprintType)
struct FCheckAliveResponse
{
GENERATED_BODY()

UPROPERTY(BlueprintReadOnly)
FString Status;

UPROPERTY(BlueprintReadOnly)
FString ErrorCode;

UPROPERTY(BlueprintReadOnly)
FString Message;
};

DECLARE_DELEGATE_OneParam(FOnCheckAliveSuccess, const FCheckAliveResponse&);
DECLARE_DELEGATE_OneParam(FOnCheckAliveError, const FCheckAliveResponse&);

UCLASS()
class YOURPROJECT_API UCheckAlive : public UObject
{
GENERATED_BODY()

public:
void Send(FOnCheckAliveSuccess OnSuccess, FOnCheckAliveError OnError);

private:
static const FString Path;
};

// CheckAlive.cpp
#include "CheckAlive.h"
#include "YourHelper.h"

const FString UCheckAlive::Path = TEXT("https://service-account.playnanoo.com/api/v20240401/alive");

void UCheckAlive::Send(FOnCheckAliveSuccess OnSuccess, FOnCheckAliveError OnError)
{
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
Request->SetVerb(TEXT("PUT"));
Request->SetURL(Path);

// 공통 헤더 설정 (인증 토큰 필요)
FYourHelper::SetCommonHeaders(Request, true);

// 요청 바디 생성
TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
JsonObject->SetStringField(TEXT("platform"), FYourHelper::GetPlatform());
JsonObject->SetStringField(TEXT("device_id"), FYourHelper::GetDeviceId());
JsonObject->SetStringField(TEXT("device_model"), FYourHelper::GetDeviceModel());
JsonObject->SetStringField(TEXT("device_os"), FYourHelper::GetDeviceOS());
JsonObject->SetStringField(TEXT("device_language"), FYourHelper::GetDeviceLanguage());

FString RequestBody;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&RequestBody);
FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);
Request->SetContentAsString(RequestBody);

Request->OnProcessRequestComplete().BindLambda(
[OnSuccess, OnError](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess)
{
FCheckAliveResponse Result;

if (bSuccess && Response.IsValid())
{
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());

if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
{
JsonObject->TryGetStringField(TEXT("Status"), Result.Status);
JsonObject->TryGetStringField(TEXT("ErrorCode"), Result.ErrorCode);
JsonObject->TryGetStringField(TEXT("Message"), Result.Message);

if (Result.ErrorCode.IsEmpty())
{
OnSuccess.ExecuteIfBound(Result);
}
else
{
OnError.ExecuteIfBound(Result);
}
}
}
else
{
Result.ErrorCode = TEXT("HTTP_ERROR");
Result.Message = TEXT("Request failed");
OnError.ExecuteIfBound(Result);
}
}
);

Request->ProcessRequest();
}

添加到单例管理器类

在单例管理器中定期调用CheckAlive并检测重复登录。

// YourSingleton.h에 추가
DECLARE_DELEGATE_OneParam(FOnDuplicateDetected, bool);

private:
bool bIsDuplicate = false;
FTimerHandle AliveTimerHandle;
FOnDuplicateDetected DuplicateCallback;

public:
// 세션 유지 시작 (최소 300초)
UFUNCTION(BlueprintCallable, Category = "YourProject|Session")
void CheckAliveStart(int32 DelayTime = 300);

// 세션 유지 중지
UFUNCTION(BlueprintCallable, Category = "YourProject|Session")
void CheckAliveStop();

// 중복 로그인 체크 콜백 등록
void CheckDuplicate(FOnDuplicateDetected Callback);

// YourSingleton.cpp에 추가
void UYourSingleton::CheckAliveStart(int32 DelayTime)
{
if (AliveTimerHandle.IsValid())
{
return;
}

int32 Timer = FMath::Max(DelayTime, 300);

GetWorld()->GetTimerManager().SetTimer(
AliveTimerHandle,
[this]()
{
if (DataManager->GetAccessToken().IsEmpty())
{
return;
}

UCheckAlive* CheckAlive = NewObject<UCheckAlive>();
CheckAlive->Send(
FOnCheckAliveSuccess::CreateLambda([](const FCheckAliveResponse& Response)
{
// 정상 응답 - 중복 아님
UE_LOG(LogTemp, Log, TEXT("Session alive: %s"), *Response.Status);
}),
FOnCheckAliveError::CreateLambda([this](const FCheckAliveResponse& Response)
{
// 30006: DuplicatedDeviceException - 다른 디바이스에서 이미 인증됨
if (Response.ErrorCode == TEXT("30006"))
{
bIsDuplicate = true;
DuplicateCallback.ExecuteIfBound(true);
}
})
);
},
Timer,
true // 반복 실행
);
}

void UYourSingleton::CheckAliveStop()
{
if (AliveTimerHandle.IsValid())
{
GetWorld()->GetTimerManager().ClearTimer(AliveTimerHandle);
AliveTimerHandle.Invalidate();
}
}

void UYourSingleton::CheckDuplicate(FOnDuplicateDetected Callback)
{
DuplicateCallback = Callback;
}

使用示例

基本用法

void AYourGameMode::BeginPlay()
{
Super::BeginPlay();

// 로그인 성공 후 세션 유지 시작 (300초마다)
UYourSingleton::Get()->CheckAliveStart(300);

// 중복 로그인 체크 콜백 등록
UYourSingleton::Get()->CheckDuplicate(
FOnDuplicateDetected::CreateLambda([](bool bIsDuplicate)
{
if (bIsDuplicate)
{
UE_LOG(LogTemp, Error, TEXT("Duplicate connection has been detected."));
// 강제 로그아웃 처리
// 예: 로그인 화면으로 이동, 토큰 삭제 등
}
})
);
}

登出时停止会话保持

void AYourGameMode::Logout()
{
// 세션 유지 중지
UYourSingleton::Get()->CheckAliveStop();

// 기타 로그아웃 처리...
}

工作流程

  1. 登录成功后调用CheckAliveStart(300)
  2. 每300秒向服务器发送alive请求
  3. 正常响应: 会话保持
  4. 错误30006: 已在其他设备登录 → bIsDuplicate = true
  5. 回调调用: 检测到重复时执行已注册的回调
  6. 回调处理: 强制登出、显示通知等

注意事项

禁止重复调用

CheckAliveStart()整个应用中只能调用一次

  • 不能在多个关卡或Actor中重复调用。
  • 重复调用时,服务器可能会将其检测为异常请求模式,导致误判(false positive)触发重复登录错误(30006)
  • 请使用单例模式在一个位置管理会话保持。
  • 虽然定时器已在运行时内部会防止重复调用,但在结构上保持单一调用点是最佳实践。