修改协议工具

This commit is contained in:
2025-07-27 23:46:43 +08:00
parent 743c1d2baa
commit be33e12b35
112 changed files with 1021 additions and 1119 deletions

View File

@@ -0,0 +1,64 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using Fantasy.Entitas.Interface;
using Microsoft.IdentityModel.Tokens;
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
namespace NB.Gate;
public sealed class GateJWTComponentAwakeSystem : AwakeSystem<GateJWTComponent>
{
protected override void Awake(GateJWTComponent self)
{
self.Awake();
}
}
public static class GateJWTComponentSystem
{
public static void Awake(this GateJWTComponent self)
{
var rsa = RSA.Create();
rsa.ImportRSAPublicKey(Convert.FromBase64String(self.PublicKeyPem), out _);
self.SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);
// 创建 TokenValidationParameters 对象,用于配置验证参数
self.TokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = false, // 禁止令牌验证时间是否过期
ValidateIssuer = true, // 验证发行者
ValidateAudience = true, // 验证受众
ValidateIssuerSigningKey = true, // 验证签名密钥
ValidIssuer = "Fantasy", // 有效的发行者
ValidAudience = "Fantasy", // 有效的受众
IssuerSigningKey = new RsaSecurityKey(rsa) // RSA公钥作为签名密钥
};
}
public static bool ValidateToken(this GateJWTComponent self, string token, out JwtPayload payload)
{
payload = null;
try
{
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
jwtSecurityTokenHandler.ValidateToken(token, self.TokenValidationParameters, out _);
payload = jwtSecurityTokenHandler.ReadJwtToken(token).Payload;
return true;
}
catch (SecurityTokenInvalidAudienceException)
{
Console.WriteLine("验证受众失败!");
return false;
}
catch (SecurityTokenInvalidIssuerException)
{
Console.WriteLine("验证发行者失败!");
return false;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}

View File

@@ -0,0 +1,35 @@
using System.IdentityModel.Tokens.Jwt;
using Fantasy;
namespace NB.Gate;
public static class GateJWTHelper
{
/// <summary>
/// 验证令牌是否合法
/// </summary>
/// <param name="scene"></param>
/// <param name="token">ToKen</param>
/// <param name="accountId">如果验证成功会返回正常的AccountId</param>
/// <returns>如果是True表示验证成功False表示验证失败</returns>
public static bool ValidateToken(Scene scene, string token, out long accountId)
{
if (!ValidateToken(scene, token, out JwtPayload payload))
{
// 如果令牌验证失败表示当前令牌不合法、那就返回为false让上层处理。
accountId = 0;
return false;
}
// 如果不等于当前Scene的ConfigId的话把就表示该连接不应该连接到当前的Gate里。
// 所以理应把当前连接关闭掉。
var sceneId = Convert.ToInt64(payload["SceneId"]);
accountId = Convert.ToInt64(payload["aId"]);
return sceneId == scene.SceneConfigId;
}
private static bool ValidateToken(Scene scene, string token, out JwtPayload payload)
{
return scene.GetComponent<GateJWTComponent>().ValidateToken(token, out payload);
}
}

View File

@@ -0,0 +1,13 @@
using Fantasy.Entitas.Interface;
namespace NB.Gate;
public sealed class PlayerDestroySystem : DestroySystem<Player>
{
protected override void Destroy(Player self)
{
self.CreateTime = 0;
self.LoginTime = 0;
self.SessionRunTimeId = 0;
}
}

View File

@@ -0,0 +1,29 @@
using Fantasy;
using Fantasy.Async;
using Fantasy.Entitas;
using Fantasy.Helper;
namespace NB.Gate;
public static class PlayerFactory
{
/// <summary>
/// 创建一个新的Player
/// </summary>
/// <param name="scene"></param>
/// <param name="aId">ToKen令牌传递过来的aId</param>
/// <param name="isSaveDataBase">是否在创建的过程中保存到数据库</param>
/// <returns></returns>
public static async FTask<Player> Create(Scene scene, long aId, bool isSaveDataBase = true)
{
var gameAccount = Entity.Create<Player>(scene, aId, false, false);
gameAccount.LoginTime = gameAccount.CreateTime = TimeHelper.Now;
if (isSaveDataBase)
{
await gameAccount.SaveDataBase();
}
return gameAccount;
}
}

View File

@@ -0,0 +1,19 @@
using Fantasy.Entitas.Interface;
namespace NB.Gate;
public sealed class PlayerFlagComponentDestroySystem : DestroySystem<PlayerFlagComponent>
{
protected override void Destroy(PlayerFlagComponent self)
{
if (self.AccountID != 0)
{
// 执行下线过程、并且要求在5分钟后完成缓存清理。也就是5分钟后会保存数据到数据库。
// 由于5分钟太长了、咱们测试的时候不方便测试也可以把这个时间改短一些比如10秒。
PlayerHelper.Disconnect(self.Scene, self.AccountID, 1000 * 60 * 5).Coroutine();
self.AccountID = 0;
}
self.Player = null;
}
}

View File

@@ -0,0 +1,113 @@
using NB.Gate.System;
using Fantasy;
using Fantasy.Async;
using Fantasy.Network;
namespace NB.Gate;
public static class PlayerHelper
{
/// <summary>
/// 从数据库中读取GameAccount
/// </summary>
/// <param name="scene"></param>
/// <param name="accountId">账号Id</param>
/// <returns></returns>
public static async FTask<Player> LoadDataBase(Scene scene, long accountId)
{
var account = await scene.World.DataBase.First<Player>(d => d.Id == accountId);
if (account == null)
{
return null;
}
account.Deserialize(scene);
return account;
}
/// <summary>
/// 保存账号到数据库中
/// </summary>
/// <param name="self"></param>
public static async FTask SaveDataBase(this Player self)
{
await self.Scene.World.DataBase.Save(self);
}
/// <summary>
/// 执行该账号的断开逻辑,不要非必要不要使用这个接口,这个接口是内部使用。
/// </summary>
/// <param name="self"></param>
public static async FTask Disconnect(this Player self)
{
// 保存该账号信息到数据库中。
await SaveDataBase(self);
// 在缓存中移除自己并且执行自己的Dispose方法。
self.Scene.GetComponent<PlayerManageComponent>().Remove(self.Id);
}
/// <summary>
/// 账号完整的断开逻辑,执行了这个接口后,该账号会完全下线。
/// </summary>
/// <param name="scene"></param>
/// <param name="accountId"></param>
/// <param name="timeOut"></param>
public static async FTask Disconnect(Scene scene, long accountId, int timeOut = 1000 * 60 * 3)
{
// 调用该方法有如下几种情况:
// 1、客户端主动断开如:退出游戏、切换账号、等。 客户端会主动发送一个协议给服务器通知服务器断开。
// 2、客户端断断线 客户端不会主动发送一个协议给服务器,是由服务器的心跳来检测是否断开了。
// 如果是心跳检测断开的Session我怎么能拿到当前的这个账号来进行下线处理呢
// 通过给当前的Session挂载一个组件当销毁这个Session时候呢也会销毁这个组件。
// 这样的话是不是可以在登录的时候给这个组件把AccountId存到这个组件呢
// 要检查当前缓存中是否存在该账号的数据
var gameAccountManageComponent = scene.GetComponent<PlayerManageComponent>();
if (!gameAccountManageComponent.TryGet(accountId, out var account))
{
// 如果缓存中没有、那表示已经下线或者根本不存在该账号,应该在打印一个警告,因为正常的情况下是不会出现的。
Log.Warning($"GameAccountHelper Disconnect accountId : {accountId} not found");
return;
}
// 为了防止逻辑的错误,加一个警告来排除下
if (!scene.TryGetEntity<Session>(account.SessionRunTimeId, out var session))
{
// 如果没有找到对应的Session那只有一种可能就是当前的链接会话已经断开了一般的情况下也不会出现的所以咱们也要打印一个警告。
Log.Warning($"GameAccountHelper Disconnect accountId : {accountId} SessionRunTimeId : {account.SessionRunTimeId} not found");
return;
}
// 如果不存在定时任务的组件,那就添加并设置定时任务
if (account.IsTimeOutComponent())
{
// 如果已经存在了,那就表示当然已经有一个延时断开的任务了,那就不需要重复添加了
return;
}
// 立即下线处理
if (timeOut <= 0)
{
// 如果timeOut销毁或等会0的情况下执行立即下线。
await account.Disconnect();
return;
}
// 设置延迟下线
account.SetTimeout(timeOut, account.Disconnect);
}
/// <summary>
/// 获得GameAccountInfo
/// </summary>
/// <param name="self"></param>
/// <returns></returns>
public static GameAccountInfo GetGameAccountInfo(this Player self)
{
// 其实可以不用每次都NEW一个新的GameAccountInfo
// 可以在当前账号下创建一个GameAccountInfo每次变动会提前通知这个GameAccountInfo
// 又或者每次调用该方法的时候,把值重新赋值一下。
return new GameAccountInfo()
{
CreateTime = self.CreateTime,
LoginTime = self.LoginTime
};
}
}

View File

@@ -0,0 +1,49 @@
using Fantasy.Entitas.Interface;
namespace NB.Gate.System;
public sealed class PlayerManageComponentDestroySystem : DestroySystem<PlayerManageComponent>
{
protected override void Destroy(PlayerManageComponent self)
{
foreach (var (_, gameAccount) in self.Players)
{
gameAccount.Dispose();
}
self.Players.Clear();
}
}
public static class PlayerManageComponentSystem
{
public static void Add(this PlayerManageComponent self, Player account)
{
self.Players.Add(account.Id, account);
}
public static Player Get(this PlayerManageComponent self, long accountId)
{
return self.Players.GetValueOrDefault(accountId);
}
public static bool TryGet(this PlayerManageComponent self, long accountId, out Player? account)
{
return self.Players.TryGetValue(accountId, out account);
}
public static void Remove(this PlayerManageComponent self, long accountId, bool isDispose = true)
{
if (!self.Players.Remove(accountId, out var account))
{
return;
}
if (!isDispose)
{
return;
}
account.Dispose();
}
}