feat : 토큰 인증 RestApi 구현 / Token기반 haskKey구현 / Dummy, User Token체크 분기
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
48
MMOTestServer/MMOserver/Api/RestApi.cs
Normal file
48
MMOTestServer/MMOserver/Api/RestApi.cs
Normal file
@@ -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<RestApi>
|
||||
{
|
||||
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<string?> 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<AuthVerifyResponse>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<AccTokenPacket>(new ReadOnlyMemory<byte>(payload));
|
||||
DummyAccTokenPacket accTokenPacket = Serializer.Deserialize<DummyAccTokenPacket>(new ReadOnlyMemory<byte>(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<AccTokenPacket>(new ReadOnlyMemory<byte>(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);
|
||||
|
||||
@@ -30,8 +30,4 @@
|
||||
<ProjectReference Include="..\ServerLib\ServerLib.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="RestApi\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
12
MMOTestServer/MMOserver/Utils/Singleton.cs
Normal file
12
MMOTestServer/MMOserver/Utils/Singleton.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace MMOserver.Utils;
|
||||
|
||||
public abstract class Singleton<T> where T : Singleton<T>, new()
|
||||
{
|
||||
private static readonly Lazy<T> instance = new Lazy<T>(static () => new T(), LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
|
||||
public static T Instance => instance.Value;
|
||||
|
||||
protected Singleton()
|
||||
{
|
||||
}
|
||||
}
|
||||
21
MMOTestServer/MMOserver/Utils/UuidGeneratorManager.cs
Normal file
21
MMOTestServer/MMOserver/Utils/UuidGeneratorManager.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace MMOserver.Utils;
|
||||
|
||||
public class UuidGeneratorManager : Singleton<UuidGeneratorManager>
|
||||
{
|
||||
private long counter = 0;
|
||||
private readonly ConcurrentDictionary<string, long> 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);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace ServerLib.Packet;
|
||||
/// </summary>
|
||||
public enum PacketType : ushort
|
||||
{
|
||||
Auth = 1, // 클라 → 서버: 최초 인증 (HashKey 전달)
|
||||
ACC_TOKEN = 1, // 클라 → 서버: 최초 인증 (HashKey 전달)
|
||||
DUMMY_ACC_TOKEN = 1001,
|
||||
// 1000번 이상은 게임 패킷으로 예약
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user