From 343ea43a03fca465186391efacd69bc18c267f32 Mon Sep 17 00:00:00 2001 From: qornwh1 Date: Wed, 4 Mar 2026 15:53:15 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=ED=86=A0=ED=81=B0=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20RestApi=20=EA=B5=AC=ED=98=84=20/=20Token=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20haskKey=EA=B5=AC=ED=98=84=20/=20Dummy,=20User=20Tok?= =?UTF-8?q?en=EC=B2=B4=ED=81=AC=20=EB=B6=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DummyService/DummyClients.cs | 4 +- .../EchoClientTester/Packet/PacketBody.cs | 14 ++++- .../EchoClientTester/Packet/PacketHeader.cs | 6 +- MMOTestServer/MMOserver/Api/RestApi.cs | 48 ++++++++++++++++ MMOTestServer/MMOserver/Game/GameServer.cs | 56 ++++++++++++++++++- MMOTestServer/MMOserver/MMOserver.csproj | 4 -- MMOTestServer/MMOserver/Packet/PacketBody.cs | 14 ++++- .../MMOserver/Packet/PacketHeader.cs | 6 +- MMOTestServer/MMOserver/Utils/Singleton.cs | 12 ++++ .../MMOserver/Utils/UuidGeneratorManager.cs | 21 +++++++ MMOTestServer/ServerLib/Packet/PacketType.cs | 3 +- MMOTestServer/ServerLib/Service/ServerBase.cs | 15 ++++- 12 files changed, 187 insertions(+), 16 deletions(-) create mode 100644 MMOTestServer/MMOserver/Api/RestApi.cs create mode 100644 MMOTestServer/MMOserver/Utils/Singleton.cs create mode 100644 MMOTestServer/MMOserver/Utils/UuidGeneratorManager.cs diff --git a/ClientTester/EchoClientTester/DummyService/DummyClients.cs b/ClientTester/EchoClientTester/DummyService/DummyClients.cs index b00572c..f9c10ce 100644 --- a/ClientTester/EchoClientTester/DummyService/DummyClients.cs +++ b/ClientTester/EchoClientTester/DummyService/DummyClients.cs @@ -63,10 +63,10 @@ public class DummyClients Log.Information("[Client {ClientId:00}] 연결됨", this.clientId); // clientID가 토큰의 hashKey라고 가정함 - AccTokenPacket recvTokenPacket = new AccTokenPacket(); + DummyAccTokenPacket recvTokenPacket = new DummyAccTokenPacket(); recvTokenPacket.Token = clientId; - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ACC_TOKEN, recvTokenPacket); + byte[] data = PacketSerializer.Serialize((ushort)PacketCode.DUMMY_ACC_TOKEN, recvTokenPacket); writer.Put(data); peer.Send(writer, DeliveryMethod.ReliableOrdered); writer.Reset(); diff --git a/ClientTester/EchoClientTester/Packet/PacketBody.cs b/ClientTester/EchoClientTester/Packet/PacketBody.cs index 81d4214..80a343d 100644 --- a/ClientTester/EchoClientTester/Packet/PacketBody.cs +++ b/ClientTester/EchoClientTester/Packet/PacketBody.cs @@ -120,12 +120,24 @@ public class PlayerInfo // 인증 // ============================================================ +// DUMMY_ACC_TOKEN +[ProtoContract] +public class DummyAccTokenPacket +{ + [ProtoMember(1)] + public long Token + { + get; + set; + } +} + // ACC_TOKEN [ProtoContract] public class AccTokenPacket { [ProtoMember(1)] - public long Token + public string Token { get; set; diff --git a/ClientTester/EchoClientTester/Packet/PacketHeader.cs b/ClientTester/EchoClientTester/Packet/PacketHeader.cs index faa6e06..1617c8f 100644 --- a/ClientTester/EchoClientTester/Packet/PacketHeader.cs +++ b/ClientTester/EchoClientTester/Packet/PacketHeader.cs @@ -2,7 +2,11 @@ namespace ClientTester.Packet; public enum PacketCode : ushort { - ECHO = 0, + // ECHO + ECHO = 1000, + + // DUMMY 클라는 이걸로 jwt토큰 안받음 + DUMMY_ACC_TOKEN = 1001, // 초기 클라이언트 시작시 jwt토큰 받아옴 ACC_TOKEN = 1, diff --git a/MMOTestServer/MMOserver/Api/RestApi.cs b/MMOTestServer/MMOserver/Api/RestApi.cs new file mode 100644 index 0000000..bc7a91f --- /dev/null +++ b/MMOTestServer/MMOserver/Api/RestApi.cs @@ -0,0 +1,48 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using MMOserver.Utils; +using Serilog; + +namespace MMOserver.Api; + +public class RestApi : Singleton +{ + private const string VERIFY_URL = "https://a301.api.tolelom.xyz/api/auth/verify"; + private readonly HttpClient httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; + + // 토큰 검증 - 성공 시 username 반환, 실패(401/타임아웃 등) 시 null 반환 + public async Task VerifyTokenAsync(string token) + { + try + { + HttpResponseMessage response = await httpClient.PostAsJsonAsync(VERIFY_URL, new { token }); + + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + Log.Warning("[RestApi] 인증 실패 (401)"); + return null; + } + + response.EnsureSuccessStatusCode(); + + AuthVerifyResponse? result = await response.Content.ReadFromJsonAsync(); + return result?.Username; + } + catch (Exception ex) + { + Log.Error("[RestApi] 웹서버 통신 실패: {Message}", ex.Message); + return null; + } + } + + private sealed class AuthVerifyResponse + { + [JsonPropertyName("username")] + public string? Username + { + get; + set; + } + } +} diff --git a/MMOTestServer/MMOserver/Game/GameServer.cs b/MMOTestServer/MMOserver/Game/GameServer.cs index 26aedfe..083855d 100644 --- a/MMOTestServer/MMOserver/Game/GameServer.cs +++ b/MMOTestServer/MMOserver/Game/GameServer.cs @@ -1,7 +1,9 @@ using LiteNetLib; using LiteNetLib.Utils; +using MMOserver.Api; using MMOserver.Game.Channel; using MMOserver.Packet; +using MMOserver.Utils; using ProtoBuf; using Serilog; using ServerLib.Packet; @@ -44,9 +46,9 @@ public class GameServer : ServerBase SendTo(peer, payload, DeliveryMethod.ReliableUnordered); } - protected override void HandleAuth(NetPeer peer, byte[] payload) + protected override void HandleAuthDummy(NetPeer peer, byte[] payload) { - AccTokenPacket accTokenPacket = Serializer.Deserialize(new ReadOnlyMemory(payload)); + DummyAccTokenPacket accTokenPacket = Serializer.Deserialize(new ReadOnlyMemory(payload)); long hashKey = accTokenPacket.Token; if (sessions.TryGetValue(hashKey, out NetPeer? existing)) @@ -68,6 +70,56 @@ public class GameServer : ServerBase ChannelManager cm = ChannelManager.Instance; cm.AddUser(1, hashKey, new Player()); } + else + { + Log.Error("[Server] Dummy 클라이언트가 아닙니다. 연결을 종료합니다. HashKey={Key} PeerId={Id}", hashKey, peer.Id); + peer.Disconnect(); + return; + } + + Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id); + OnSessionConnected(peer, hashKey); + } + + protected override async void HandleAuth(NetPeer peer, byte[] payload) + { + AccTokenPacket accTokenPacket = Serializer.Deserialize(new ReadOnlyMemory(payload)); + string token = accTokenPacket.Token; + long hashKey = UuidGeneratorManager.Instance.GetOrCreate(token); + + if (sessions.TryGetValue(hashKey, out NetPeer? existing)) + { + // WiFi → LTE 전환 등 재연결: 이전 피어 교체 (토큰 재검증 불필요) + existing.Tag = null; + sessions.Remove(hashKey); + Log.Information("[Server] 재연결 HashKey={Key} Old={Old} New={New}", hashKey, existing.Id, peer.Id); + existing.Disconnect(); + } + else + { + // 신규 연결: 웹서버에 JWT 검증 요청 + string? username = await RestApi.Instance.VerifyTokenAsync(token); + if (username == null) + { + Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id); + peer.Disconnect(); + return; + } + + Log.Information("[Server] 토큰 검증 성공 Username={Username} PeerId={Id}", username, peer.Id); + } + + peer.Tag = new Session(hashKey, peer); + sessions[hashKey] = peer; + pendingPeers.Remove(peer.Id); + + if (hashKey <= 1000) + { + // 더미 클라이언트면 에러 + Log.Error("[Server] Dummy 클라이언트가 입니다. 연결을 종료합니다. HashKey={Key} PeerId={Id}", hashKey, peer.Id); + peer.Disconnect(); + return; + } Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id); OnSessionConnected(peer, hashKey); diff --git a/MMOTestServer/MMOserver/MMOserver.csproj b/MMOTestServer/MMOserver/MMOserver.csproj index d8324ce..3f9edfb 100644 --- a/MMOTestServer/MMOserver/MMOserver.csproj +++ b/MMOTestServer/MMOserver/MMOserver.csproj @@ -30,8 +30,4 @@ - - - - diff --git a/MMOTestServer/MMOserver/Packet/PacketBody.cs b/MMOTestServer/MMOserver/Packet/PacketBody.cs index 171cda1..a3440a7 100644 --- a/MMOTestServer/MMOserver/Packet/PacketBody.cs +++ b/MMOTestServer/MMOserver/Packet/PacketBody.cs @@ -120,12 +120,24 @@ public class PlayerInfo // 인증 // ============================================================ +// DUMMY_ACC_TOKEN +[ProtoContract] +public class DummyAccTokenPacket +{ + [ProtoMember(1)] + public long Token + { + get; + set; + } +} + // ACC_TOKEN [ProtoContract] public class AccTokenPacket { [ProtoMember(1)] - public long Token + public string Token { get; set; diff --git a/MMOTestServer/MMOserver/Packet/PacketHeader.cs b/MMOTestServer/MMOserver/Packet/PacketHeader.cs index c8630ee..204d965 100644 --- a/MMOTestServer/MMOserver/Packet/PacketHeader.cs +++ b/MMOTestServer/MMOserver/Packet/PacketHeader.cs @@ -2,7 +2,11 @@ namespace MMOserver.Packet; public enum PacketCode : ushort { - ECHO = 0, + // ECHO + ECHO = 1000, + + // DUMMY 클라는 이걸로 jwt토큰 안받음 + DUMMY_ACC_TOKEN = 1001, // 초기 클라이언트 시작시 jwt토큰 받아옴 ACC_TOKEN = 1, diff --git a/MMOTestServer/MMOserver/Utils/Singleton.cs b/MMOTestServer/MMOserver/Utils/Singleton.cs new file mode 100644 index 0000000..7309c0d --- /dev/null +++ b/MMOTestServer/MMOserver/Utils/Singleton.cs @@ -0,0 +1,12 @@ +namespace MMOserver.Utils; + +public abstract class Singleton where T : Singleton, new() +{ + private static readonly Lazy instance = new Lazy(static () => new T(), LazyThreadSafetyMode.ExecutionAndPublication); + + public static T Instance => instance.Value; + + protected Singleton() + { + } +} diff --git a/MMOTestServer/MMOserver/Utils/UuidGeneratorManager.cs b/MMOTestServer/MMOserver/Utils/UuidGeneratorManager.cs new file mode 100644 index 0000000..1c0ac58 --- /dev/null +++ b/MMOTestServer/MMOserver/Utils/UuidGeneratorManager.cs @@ -0,0 +1,21 @@ +using System.Collections.Concurrent; + +namespace MMOserver.Utils; + +public class UuidGeneratorManager : Singleton +{ + private long counter = 0; + private readonly ConcurrentDictionary tokenMap = new(); + + // string token → 고유 long ID 변환 (멀티스레드 안전) + // 동일 token은 항상 같은 ID 반환 + public long GetOrCreate(string token) + { + return tokenMap.GetOrAdd(token, _ => Interlocked.Increment(ref counter)); + } + + public bool TryGet(string token, out long id) + { + return tokenMap.TryGetValue(token, out id); + } +} diff --git a/MMOTestServer/ServerLib/Packet/PacketType.cs b/MMOTestServer/ServerLib/Packet/PacketType.cs index 5aaece0..5862537 100644 --- a/MMOTestServer/ServerLib/Packet/PacketType.cs +++ b/MMOTestServer/ServerLib/Packet/PacketType.cs @@ -6,6 +6,7 @@ namespace ServerLib.Packet; /// public enum PacketType : ushort { - Auth = 1, // 클라 → 서버: 최초 인증 (HashKey 전달) + ACC_TOKEN = 1, // 클라 → 서버: 최초 인증 (HashKey 전달) + DUMMY_ACC_TOKEN = 1001, // 1000번 이상은 게임 패킷으로 예약 } diff --git a/MMOTestServer/ServerLib/Service/ServerBase.cs b/MMOTestServer/ServerLib/Service/ServerBase.cs index e39fcee..c165089 100644 --- a/MMOTestServer/ServerLib/Service/ServerBase.cs +++ b/MMOTestServer/ServerLib/Service/ServerBase.cs @@ -149,18 +149,23 @@ public abstract class ServerBase : INetEventListener } // 0이라면 에코 서버 테스트용 따로 처리 - if (type == 0) + if (type == 1000) { HandleEcho(peer, data); return; } // Auth 패킷은 베이스에서 처리 (raw 8-byte long, protobuf 불필요) - if (type == (ushort)PacketType.Auth) + if (type == (ushort)PacketType.ACC_TOKEN) { HandleAuth(peer, payload); return; } + else if (type == (ushort)PacketType.DUMMY_ACC_TOKEN) + { + HandleAuthDummy(peer, payload); + return; + } // 인증된 피어인지 확인 if (peer.Tag is not Session session) @@ -204,7 +209,11 @@ public abstract class ServerBase : INetEventListener protected abstract void HandleEcho(NetPeer peer, byte[] payload); - // ─── Auth 처리 (내부) ──────────────────────────────────────────────── + // ─── Auth 처리 (더미) ──────────────────────────────────────────────── + + protected abstract void HandleAuthDummy(NetPeer peer, byte[] payload); + + // ─── Auth 처리 ──────────────────────────────────────────────── protected abstract void HandleAuth(NetPeer peer, byte[] payload);