fix: MMO 서버 로직 버그 6건 수정
1. PlayerId 스푸핑 방지: OnTransformPlayer, OnActionPlayer, OnStatePlayer에서 브로드캐스트 전 packet.PlayerId = hashKey로 강제 교체 2. HP/MP 클라이언트 조작 방지: OnStatePlayer에서 범위 클램핑 (0 ≤ Hp ≤ MaxHp, 0 ≤ Mp ≤ MaxMp) 3. CreateParty 파티원 등록 누락 수정: - memberIds 파라미터 사용 시 모든 멤버를 playerPartyMap에 등록 - 리더 중복 추가 방지 (Contains 체크) 4. OnIntoChannel 채널 만석 유령 상태 방지: 이전 채널 제거 후 새 채널 입장 실패 시 이전 채널로 복귀 5. HandleAuth async 경합 방지: authenticatingTokens HashSet으로 동일 토큰 동시 인증 차단 6. 레이드 맵 미반환 수정: TryReleaseRaidMap 헬퍼 추가, OnChangeMap/OnSessionDisconnected에서 레이드 맵(1001+) 유저 0명 시 인스턴스 맵 해제 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,9 @@ public class GameServer : ServerBase
|
|||||||
private readonly Dictionary<ushort, Action<NetPeer, int, byte[]>> packetHandlers;
|
private readonly Dictionary<ushort, Action<NetPeer, int, byte[]>> packetHandlers;
|
||||||
private readonly UuidGenerator userUuidGenerator;
|
private readonly UuidGenerator userUuidGenerator;
|
||||||
|
|
||||||
|
// 동일 토큰 동시 인증 방지
|
||||||
|
private readonly HashSet<string> authenticatingTokens = new();
|
||||||
|
|
||||||
public GameServer(int port, string connectionString) : base(port, connectionString)
|
public GameServer(int port, string connectionString) : base(port, connectionString)
|
||||||
{
|
{
|
||||||
packetHandlers = new Dictionary<ushort, Action<NetPeer, int, byte[]>>
|
packetHandlers = new Dictionary<ushort, Action<NetPeer, int, byte[]>>
|
||||||
@@ -103,49 +106,65 @@ public class GameServer : ServerBase
|
|||||||
{
|
{
|
||||||
AccTokenPacket accTokenPacket = Serializer.Deserialize<AccTokenPacket>(new ReadOnlyMemory<byte>(payload));
|
AccTokenPacket accTokenPacket = Serializer.Deserialize<AccTokenPacket>(new ReadOnlyMemory<byte>(payload));
|
||||||
string token = accTokenPacket.Token;
|
string token = accTokenPacket.Token;
|
||||||
string username = "";
|
|
||||||
tokenHash.TryGetValue(token, out int hashKey);
|
// 동일 토큰 동시 인증 방지
|
||||||
if (hashKey <= 1000)
|
if (!authenticatingTokens.Add(token))
|
||||||
{
|
{
|
||||||
hashKey = userUuidGenerator.Create();
|
Log.Warning("[Server] 동일 토큰 동시 인증 시도 차단 PeerId={Id}", peer.Id);
|
||||||
|
peer.Disconnect();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessions.TryGetValue(hashKey, out NetPeer? existing))
|
try
|
||||||
{
|
{
|
||||||
// WiFi → LTE 전환 등 재연결: 이전 피어 교체 (토큰 재검증 불필요)
|
string username = "";
|
||||||
existing.Tag = null;
|
tokenHash.TryGetValue(token, out int hashKey);
|
||||||
sessions.Remove(hashKey);
|
if (hashKey <= 1000)
|
||||||
Log.Information("[Server] 재연결 HashKey={Key} Old={Old} New={New}", hashKey, existing.Id, peer.Id);
|
|
||||||
existing.Disconnect();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 신규 연결: 웹서버에 JWT 검증 요청
|
|
||||||
username = await RestApi.Instance.VerifyTokenAsync(token);
|
|
||||||
if (username == null)
|
|
||||||
{
|
{
|
||||||
Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id);
|
hashKey = userUuidGenerator.Create();
|
||||||
userUuidGenerator.Release(hashKey);
|
|
||||||
peer.Disconnect();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Information("[Server] 토큰 검증 성공 Username={Username} PeerId={Id}", username, peer.Id);
|
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 검증 요청
|
||||||
|
username = await RestApi.Instance.VerifyTokenAsync(token);
|
||||||
|
if (username == null)
|
||||||
|
{
|
||||||
|
Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id);
|
||||||
|
userUuidGenerator.Release(hashKey);
|
||||||
|
peer.Disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
peer.Tag = new Session(hashKey, peer);
|
Log.Information("[Server] 토큰 검증 성공 Username={Username} PeerId={Id}", username, peer.Id);
|
||||||
((Session)peer.Tag).Token = token;
|
}
|
||||||
if (username.Length > 0)
|
|
||||||
|
peer.Tag = new Session(hashKey, peer);
|
||||||
|
((Session)peer.Tag).Token = token;
|
||||||
|
if (username.Length > 0)
|
||||||
|
{
|
||||||
|
((Session)peer.Tag).UserName = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions[hashKey] = peer;
|
||||||
|
tokenHash[token] = hashKey;
|
||||||
|
pendingPeers.Remove(peer.Id);
|
||||||
|
|
||||||
|
Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id);
|
||||||
|
OnSessionConnected(peer, hashKey);
|
||||||
|
}
|
||||||
|
finally
|
||||||
{
|
{
|
||||||
((Session)peer.Tag).UserName = username;
|
authenticatingTokens.Remove(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions[hashKey] = peer;
|
|
||||||
tokenHash[token] = hashKey;
|
|
||||||
pendingPeers.Remove(peer.Id);
|
|
||||||
|
|
||||||
Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id);
|
|
||||||
OnSessionConnected(peer, hashKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnSessionConnected(NetPeer peer, int hashKey)
|
protected override void OnSessionConnected(NetPeer peer, int hashKey)
|
||||||
@@ -186,6 +205,9 @@ public class GameServer : ServerBase
|
|||||||
// 파티 자동 탈퇴
|
// 파티 자동 탈퇴
|
||||||
HandlePartyLeaveOnExit(channelId, hashKey);
|
HandlePartyLeaveOnExit(channelId, hashKey);
|
||||||
|
|
||||||
|
// 레이드 맵이었으면 해제 체크
|
||||||
|
TryReleaseRaidMap(cm.GetChannel(channelId), player.CurrentMapId);
|
||||||
|
|
||||||
// 같은 채널 유저들에게 나갔다고 알림
|
// 같은 채널 유저들에게 나갔다고 알림
|
||||||
SendExitChannelPacket(peer, hashKey, channelId, player);
|
SendExitChannelPacket(peer, hashKey, channelId, player);
|
||||||
}
|
}
|
||||||
@@ -447,6 +469,20 @@ public class GameServer : ServerBase
|
|||||||
{
|
{
|
||||||
Log.Warning("[GameServer] INTO_CHANNEL 채널 인원 초과 HashKey={Key} ChannelId={ChannelId} UserCount={Count}/{Max}",
|
Log.Warning("[GameServer] INTO_CHANNEL 채널 인원 초과 HashKey={Key} ChannelId={ChannelId} UserCount={Count}/{Max}",
|
||||||
hashKey, packet.ChannelId, newChannel.UserCount, newChannel.UserCountMax);
|
hashKey, packet.ChannelId, newChannel.UserCount, newChannel.UserCountMax);
|
||||||
|
|
||||||
|
// 이전 채널에서 이미 제거된 경우 → 이전 채널로 복귀
|
||||||
|
if (preChannelId >= 0)
|
||||||
|
{
|
||||||
|
Player? fallbackPlayer = new()
|
||||||
|
{
|
||||||
|
HashKey = hashKey,
|
||||||
|
PlayerId = hashKey,
|
||||||
|
Nickname = ((Session)peer.Tag).UserName
|
||||||
|
};
|
||||||
|
cm.AddUser(preChannelId, hashKey, fallbackPlayer, peer);
|
||||||
|
Log.Information("[GameServer] INTO_CHANNEL 만석 → 이전 채널({ChannelId})로 복귀 HashKey={Key}", preChannelId, hashKey);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] full = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL,
|
byte[] full = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL,
|
||||||
new IntoChannelPacket { ChannelId = -1 });
|
new IntoChannelPacket { ChannelId = -1 });
|
||||||
SendTo(peer, full);
|
SendTo(peer, full);
|
||||||
@@ -662,6 +698,9 @@ public class GameServer : ServerBase
|
|||||||
player.PosZ = packet.Position.Z;
|
player.PosZ = packet.Position.Z;
|
||||||
player.RotY = packet.RotY;
|
player.RotY = packet.RotY;
|
||||||
|
|
||||||
|
// PlayerId 강제 교체 (클라이언트 스푸핑 방지)
|
||||||
|
packet.PlayerId = hashKey;
|
||||||
|
|
||||||
// 같은 맵 유저들에게 위치/방향 브로드캐스트 (나 제외)
|
// 같은 맵 유저들에게 위치/방향 브로드캐스트 (나 제외)
|
||||||
byte[] data = PacketSerializer.Serialize((ushort)PacketCode.TRANSFORM_PLAYER, packet);
|
byte[] data = PacketSerializer.Serialize((ushort)PacketCode.TRANSFORM_PLAYER, packet);
|
||||||
BroadcastToMap(channelId, player.CurrentMapId, data, peer, DeliveryMethod.Unreliable);
|
BroadcastToMap(channelId, player.CurrentMapId, data, peer, DeliveryMethod.Unreliable);
|
||||||
@@ -684,6 +723,9 @@ public class GameServer : ServerBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PlayerId 강제 교체 (클라이언트 스푸핑 방지)
|
||||||
|
packet.PlayerId = hashKey;
|
||||||
|
|
||||||
// 같은 맵 유저들에게 행동 브로드캐스트 (나 제외)
|
// 같은 맵 유저들에게 행동 브로드캐스트 (나 제외)
|
||||||
byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ACTION_PLAYER, packet);
|
byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ACTION_PLAYER, packet);
|
||||||
BroadcastToMap(channelId, player.CurrentMapId, data, peer);
|
BroadcastToMap(channelId, player.CurrentMapId, data, peer);
|
||||||
@@ -707,6 +749,15 @@ public class GameServer : ServerBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PlayerId 강제 교체 (클라이언트 스푸핑 방지)
|
||||||
|
packet.PlayerId = hashKey;
|
||||||
|
|
||||||
|
// HP/MP 범위 클램핑 (클라이언트 조작 방지)
|
||||||
|
if (packet.MaxHp < 0) packet.MaxHp = 0;
|
||||||
|
if (packet.MaxMp < 0) packet.MaxMp = 0;
|
||||||
|
packet.Hp = Math.Clamp(packet.Hp, 0, packet.MaxHp);
|
||||||
|
packet.Mp = Math.Clamp(packet.Mp, 0, packet.MaxMp);
|
||||||
|
|
||||||
player.Hp = packet.Hp;
|
player.Hp = packet.Hp;
|
||||||
player.MaxHp = packet.MaxHp;
|
player.MaxHp = packet.MaxHp;
|
||||||
player.Mp = packet.Mp;
|
player.Mp = packet.Mp;
|
||||||
@@ -957,6 +1008,10 @@ public class GameServer : ServerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
SendTo(peer, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, response));
|
SendTo(peer, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, response));
|
||||||
|
|
||||||
|
// 레이드 맵(1001+)에서 나갔고 남은 유저가 0이면 인스턴스 맵 해제
|
||||||
|
TryReleaseRaidMap(channel, oldMapId);
|
||||||
|
|
||||||
Log.Debug("[GameServer] CHANGE_MAP HashKey={Key} OldMap={OldMapId} NewMap={MapId}", hashKey, oldMapId, packet.MapId);
|
Log.Debug("[GameServer] CHANGE_MAP HashKey={Key} OldMap={OldMapId} NewMap={MapId}", hashKey, oldMapId, packet.MapId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1188,6 +1243,22 @@ public class GameServer : ServerBase
|
|||||||
BroadcastToChannel(channelId, data);
|
BroadcastToChannel(channelId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 레이드 맵(1001+)에서 유저가 빠졌을 때, 남은 유저가 0이면 인스턴스 맵 해제
|
||||||
|
private static void TryReleaseRaidMap(Channel.Channel channel, int mapId)
|
||||||
|
{
|
||||||
|
if (mapId < 1001)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AMap? map = channel.GetMap(mapId);
|
||||||
|
if (map != null && map.GetUsers().Count == 0)
|
||||||
|
{
|
||||||
|
channel.RemoveInstanceMap(mapId);
|
||||||
|
Log.Debug("[GameServer] 레이드 맵 해제 MapId={MapId}", mapId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void SendError(NetPeer peer, ErrorCode code)
|
private void SendError(NetPeer peer, ErrorCode code)
|
||||||
{
|
{
|
||||||
ErrorPacket err = new() { Code = code };
|
ErrorPacket err = new() { Code = code };
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ public class PartyManager
|
|||||||
memeberIds = new List<int>();
|
memeberIds = new List<int>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 리더 중복 방지: 기존 멤버 목록에 리더가 없을 때만 추가
|
||||||
|
if (!memeberIds.Contains(leaderId))
|
||||||
|
{
|
||||||
|
memeberIds.Add(leaderId);
|
||||||
|
}
|
||||||
|
|
||||||
party = new PartyInfo
|
party = new PartyInfo
|
||||||
{
|
{
|
||||||
PartyId = partyId,
|
PartyId = partyId,
|
||||||
@@ -35,10 +41,14 @@ public class PartyManager
|
|||||||
PartyName = partyName,
|
PartyName = partyName,
|
||||||
PartyMemberIds = memeberIds
|
PartyMemberIds = memeberIds
|
||||||
};
|
};
|
||||||
party.PartyMemberIds.Add(leaderId);
|
|
||||||
|
|
||||||
parties[partyId] = party;
|
parties[partyId] = party;
|
||||||
playerPartyMap[leaderId] = partyId;
|
|
||||||
|
// 모든 멤버를 playerPartyMap에 등록
|
||||||
|
foreach (int memberId in memeberIds)
|
||||||
|
{
|
||||||
|
playerPartyMap[memberId] = partyId;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user