🔐 访问验证

密码错误,请重试

🎮 maimai DX Net层代码差异分析

版本 1.52 → 1.53 网络层完整代码对比

🔐
AES密钥更换
🔑
Token认证机制
🍪
Cookie会话管理
🗑️
PlaylogList删除

📋 变更摘要

1. AES加密密钥更换
Key: a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R o2U8F6<adcYl25f_qwx_n]5_qxRcbLN>
IV: d6xHIKq]1J]Dt^ue AL<G:k:X6Vu7@_U]
Mai-Encoding: 1.50 1.53
2. Token认证机制
登录响应返回 token,后续请求需携带此token
涉及API: UserLogin, UserPreview, UpsertUserAll
3. 时间字段变更
dateTime loginDateTime
登录时记录实际登录时间,登出时使用登录时的时间戳
4. Cookie会话管理
NetHttpClient新增CookieContainer支持
OperationManager新增Cookie和Token的存储管理
5. Playlog上传方式变更
删除 PacketUploadUserPlaylogListUserPlaylogListRequestVO
Playlog改为在 UpsertUserAll 中通过 userPlaylogList 字段上传
🔄 认证流程对比

1.52 旧流程:

Login API请求 Logout(dateTime)

1.53 新流程:

Login → 获取token API请求(携带token+cookie) Logout(loginDateTime)

📝 详细代码对比

🔐 CipherAES - 加密层 密钥更换
Net/CipherAES.cs
1.52
internal class CipherAES : Singleton<CipherAES> { private static readonly int BLOCK_SIZE = 128; private static readonly int KEY_SIZE = 256; private static readonly string AesKey = "a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R"; private static readonly string AesIV = "d6xHIKq]1J]Dt^ue"; public static byte[] Encrypt(byte[] data) { using (AesManaged aesManaged = new AesManaged()) { aesManaged.KeySize = KEY_SIZE; aesManaged.BlockSize = BLOCK_SIZE; aesManaged.Mode = CipherMode.CBC; aesManaged.Key = Encoding.UTF8.GetBytes(AesKey); aesManaged.IV = Encoding.UTF8.GetBytes(AesIV); aesManaged.Padding = PaddingMode.PKCS7; return aesManaged.CreateEncryptor() .TransformFinalBlock(data, 0, data.Length); } } public static byte[] Decrypt(byte[] encryptData) { // 同样使用AesKey和AesIV } }
1.53
internal class CipherAES : Singleton<CipherAES> { private static readonly int BLOCK_SIZE = 128; private static readonly int KEY_SIZE = 256; private static readonly string AesKey = "o2U8F6<adcYl25f_qwx_n]5_qxRcbLN>"; private static readonly string AesIV = "AL<G:k:X6Vu7@_U]"; public static byte[] Encrypt(byte[] data) { using (AesManaged aesManaged = new AesManaged()) { aesManaged.KeySize = KEY_SIZE; aesManaged.BlockSize = BLOCK_SIZE; aesManaged.Mode = CipherMode.CBC; aesManaged.Key = Encoding.UTF8.GetBytes(AesKey); aesManaged.IV = Encoding.UTF8.GetBytes(AesIV); aesManaged.Padding = PaddingMode.PKCS7; return aesManaged.CreateEncryptor() .TransformFinalBlock(data, 0, data.Length); } } public static byte[] Decrypt(byte[] encryptData) { // 同样使用AesKey和AesIV } }
🌐 NetHttpClient - HTTP客户端 Cookie支持
Net/NetHttpClient.cs - 字段变更
1.52
public class NetHttpClient { protected HttpWebRequest _request; protected HttpWebResponse _response; protected WaitHandle _waitHandle; protected RegisteredWaitHandle _timeoutWaitHandle; protected readonly RequestCachePolicy _cachePolicy; protected Stream _responseStream; protected readonly byte[] _buffer = new byte[1024]; protected readonly MemoryStream _temporaryStream; protected readonly MemoryStream _memoryStream; protected int _state; protected byte[] _bytes; private readonly string DefaultHttpHeaderEncryption = "1.50"; }
1.53
public class NetHttpClient { protected HttpWebRequest _request; protected HttpWebResponse _response; protected WaitHandle _waitHandle; protected RegisteredWaitHandle _timeoutWaitHandle; protected readonly RequestCachePolicy _cachePolicy; protected Stream _responseStream; protected readonly byte[] _buffer = new byte[1024]; protected readonly MemoryStream _temporaryStream; protected readonly MemoryStream _memoryStream; protected int _state; protected byte[] _bytes; protected CookieCollection _cookie; private readonly string DefaultHttpHeaderEncryption = "1.53"; }
Net/NetHttpClient.cs - Create方法
1.52
public static NetHttpClient Create(string url) { NetHttpClient netHttpClient = new NetHttpClient(); try { // ... hostBytes初始化 ... netHttpClient._request = (WebRequest.Create(url) as HttpWebRequest); netHttpClient._request.CachePolicy = netHttpClient._cachePolicy; netHttpClient.State = 1; // ... SSL设置 ... return netHttpClient; } catch (Exception) { return null; } }
1.53
public static NetHttpClient Create(string url) { NetHttpClient netHttpClient = new NetHttpClient(); try { // ... hostBytes初始化 ... netHttpClient._request = (WebRequest.Create(url) as HttpWebRequest); netHttpClient._request.CookieContainer = new CookieContainer(); netHttpClient._request.CachePolicy = netHttpClient._cachePolicy; netHttpClient.State = 1; // ... SSL设置 ... return netHttpClient; } catch (Exception) { return null; } }
Net/NetHttpClient.cs - 新增方法
1.52
public MemoryStream GetResponse() { return this._memoryStream; } private void SetSuccess(int state) { this.WebException = WebExceptionStatus.Success; this.Error = string.Empty; this.DestroyReponse(); this.State = state; }
1.53
public MemoryStream GetResponse() { return this._memoryStream; } public CookieCollection GetCookie() { return this._cookie; } public void AddCookie(ulong userId) { CookieContainer cookie = Singleton<OperationManager>.Instance.GetCookie(userId); if (cookie != null) { this._request.CookieContainer = cookie; } } private void SetSuccess(int state) { this.WebException = WebExceptionStatus.Success; this.Error = string.Empty; this._cookie = this._response.Cookies; this.DestroyReponse(); this.State = state; }
🔑 UserLoginApi - 用户登录 Token认证
Net/VO/Mai2/UserLoginRequestVO.cs
1.52
[Serializable] public class UserLoginRequestVO : VOSerializer { public ulong userId; public string accessCode; public int regionId; public int placeId; public string clientId; public long dateTime; public bool isContinue; public int genericFlag; }
1.53
[Serializable] public class UserLoginRequestVO : VOSerializer { public ulong userId; public string accessCode; public int regionId; public int placeId; public string clientId; public long dateTime; public long loginDateTime; public bool isContinue; public int genericFlag; public string token; }
Net/VO/Mai2/UserLoginResponseVO.cs
1.52
[Serializable] public class UserLoginResponseVO : VOSerializer { public int returnCode; public string lastLoginDate; public int loginCount; public int consecutiveLoginCount; public ulong loginId; }
1.53
[Serializable] public class UserLoginResponseVO : VOSerializer { public int returnCode; public string lastLoginDate; public int loginCount; public int consecutiveLoginCount; public ulong loginId; public long loginDateTime; public string token; }
Net/Packet/Mai2/PacketUserLogin.cs - 构造函数
1.52
public PacketUserLogin( ulong userId, string acsessCode, bool isContinue, int genericFlag, Action<UserLoginResponseVO> onDone, Action<PacketStatus> onError = null) { this._onDone = onDone; this._onError = onError; base.Create(new NetQuery< UserLoginRequestVO, UserLoginResponseVO>("UserLoginApi", userId) { Request = { userId = userId }, Request = { accessCode = acsessCode }, Request = { regionId = Auth.RegionCode }, Request = { placeId = (int)Auth.LocationId }, Request = { clientId = System.KeychipId.ShortValue }, Request = { dateTime = new DateTimeOffset( Auth.AuthTime).ToUnixTimeSeconds() }, Request = { isContinue = isContinue }, Request = { genericFlag = genericFlag } }, NetConfig.TimeOutInMSecLong, -1, -1); }
1.53
public PacketUserLogin( ulong userId, string acsessCode, bool isContinue, int genericFlag, Action<UserLoginResponseVO> onDone, Action<PacketStatus> onError = null) { this._onDone = onDone; this._onError = onError; base.Create(new NetQuery< UserLoginRequestVO, UserLoginResponseVO>("UserLoginApi", userId) { Request = { userId = userId }, Request = { accessCode = acsessCode }, Request = { regionId = Auth.RegionCode }, Request = { placeId = (int)Auth.LocationId }, Request = { clientId = System.KeychipId.ShortValue }, Request = { dateTime = new DateTimeOffset( Auth.AuthTime).ToUnixTimeSeconds() }, Request = { loginDateTime = new DateTimeOffset( MAI2.DateTime.Now).ToUnixTimeSeconds() }, Request = { isContinue = isContinue }, Request = { genericFlag = genericFlag }, Request = { token = Singleton<OperationManager> .Instance.GetToken(userId) } }, NetConfig.TimeOutInMSecLong, -1, -1); }
Net/Packet/Mai2/PacketUserLogin.cs - Proc方法
1.52
public override PacketState Proc() { PacketState packetState = base.ProcImpl(); if (packetState != PacketState.Done) { if (packetState == PacketState.Error) { this._onError?.Invoke(base.Status); // ... error handling ... } } else { NetQuery<UserLoginRequestVO, UserLoginResponseVO> netQuery = base.Query as NetQuery< UserLoginRequestVO, UserLoginResponseVO>; this._onDone(netQuery.Response); } return base.State; }
1.53
public override PacketState Proc() { PacketState packetState = base.ProcImpl(); if (packetState != PacketState.Done) { if (packetState == PacketState.Error) { this._onError?.Invoke(base.Status); // ... error handling ... } } else { NetQuery<UserLoginRequestVO, UserLoginResponseVO> netQuery = base.Query as NetQuery< UserLoginRequestVO, UserLoginResponseVO>; netQuery.Response.loginDateTime = netQuery.Request.loginDateTime; this._onDone(netQuery.Response); } return base.State; }
📤 UserLogoutApi - 用户登出 时间字段变更
Net/VO/Mai2/UserLogoutRequestVO.cs
1.52
[Serializable] public class UserLogoutRequestVO : VOSerializer { public ulong userId; public string accessCode; public int regionId; public int placeId; public string clientId; public long dateTime; public int type; }
1.53
[Serializable] public class UserLogoutRequestVO : VOSerializer { public ulong userId; public string accessCode; public int regionId; public int placeId; public string clientId; public long loginDateTime; public int type; }
Net/Packet/Mai2/PacketUserLogout.cs - 构造函数签名
1.52
public PacketUserLogout( ulong userId, LogoutType logoutType, string acsessCode, Action onDone, Action<PacketStatus> onError = null) { // ... netQuery.Request.dateTime = new DateTimeOffset( Auth.AuthTime).ToUnixTimeSeconds(); // ... }
1.53
public PacketUserLogout( int index, ulong userId, LogoutType logoutType, string acsessCode, Action onDone, Action<PacketStatus> onError = null) { // ... netQuery.Request.loginDateTime = Singleton<NetDataManager>.Instance .GetLoginVO(index).loginDateTime; // ... }
👤 GetUserPreviewApi - 用户预览 新增认证字段
Net/VO/Mai2/UserPreviewRequestVO.cs
1.52
[Serializable] public class UserPreviewRequestVO : VOSerializer { public ulong userId; public string segaIdAuthKey; }
1.53
[Serializable] public class UserPreviewRequestVO : VOSerializer { public ulong userId; public string segaIdAuthKey; public string token; public string clientId; }
Net/VO/Mai2/UserPreviewResponseVO.cs
1.52
[Serializable] public class UserPreviewResponseVO : VOSerializer { public ulong userId; public string userName; public bool isLogin; public string lastGameId; public string lastDataVersion; public string lastRomVersion; public string lastLoginDate; public string lastPlayDate; public int playerRating; public int nameplateId; public int iconId; public int trophyId; public int partnerId; public int frameId; public int dispRate; public int totalAwake; public int isNetMember; public string dailyBonusDate; public int headPhoneVolume; public bool isInherit; public int banState; }
1.53
[Serializable] public class UserPreviewResponseVO : VOSerializer { public ulong userId; public string userName; public bool isLogin; public string lastGameId; public string lastDataVersion; public string lastRomVersion; public string lastLoginDate; public string lastPlayDate; public int playerRating; public int nameplateId; public int iconId; public int trophyId; public int partnerId; public int frameId; public int dispRate; public int totalAwake; public int isNetMember; public string dailyBonusDate; public int headPhoneVolume; public bool isInherit; public int banState; public int errorId; }
Net/Packet/Mai2/PacketGetUserPreview.cs - 构造函数
1.52
public PacketGetUserPreview( ulong userId, string authKey, Action<ulong, UserPreviewResponseVO> onDone, Action<PacketStatus> onError = null) { this._onDone = onDone; this._onError = onError; this._userId = userId; base.Create(new NetQuery< UserPreviewRequestVO, UserPreviewResponseVO>("GetUserPreviewApi", userId) { Request = { userId = userId }, Request = { segaIdAuthKey = authKey } }, -1, -1, -1); }
1.53
public PacketGetUserPreview( ulong userId, string authKey, string token, Action<ulong, UserPreviewResponseVO> onDone, Action<PacketStatus> onError = null) { this._onDone = onDone; this._onError = onError; this._userId = userId; base.Create(new NetQuery< UserPreviewRequestVO, UserPreviewResponseVO>("GetUserPreviewApi", userId) { Request = { userId = userId }, Request = { segaIdAuthKey = authKey }, Request = { token = token }, Request = { clientId = System.KeychipId.ShortValue } }, -1, -1, -1); }
💾 UpsertUserAllApi - 用户数据上传 Playlog整合
Net/VO/Mai2/UserAllRequestVO.cs
1.52
[Serializable] public class UserAllRequestVO : VOSerializer { public ulong userId; public ulong playlogId; public bool isEventMode; public bool isFreePlay; public UserAll upsertUserAll; }
1.53
[Serializable] public class UserAllRequestVO : VOSerializer { public ulong userId; public ulong playlogId; public bool isEventMode; public bool isFreePlay; public long loginDateTime; public UserPlaylog[] userPlaylogList; public UserAll upsertUserAll; }
Net/Packet/Mai2/PacketUpsertUserAll.cs - 构造函数
1.52
public PacketUpsertUserAll( int index, UserData src, Action<int> onDone, Action<PacketStatus> onError = null) : this(index, UserID.IsGuest(src.Detail.UserID) ? UserID.GuestID(Auth.LocationId) : src.Detail.UserID, ToUserAll(src), onDone, onError) { } public PacketUpsertUserAll( int index, ulong userId, UserAll src, Action<int> onDone, Action<PacketStatus> onError = null) { // ... netQuery.Request.userId = userId; netQuery.Request.playlogId = loginVO?.loginId ?? 0UL; netQuery.Request.isEventMode = GameManager.IsEventMode; netQuery.Request.isFreePlay = AmManager.Instance.Credit.IsFreePlay(); netQuery.Request.upsertUserAll = src; // ... }
1.53
public PacketUpsertUserAll( int index, UserData src, int maxTrackNo, Action<int> onDone, Action<PacketStatus> onError = null) : this(index, UserID.IsGuest(src.Detail.UserID) ? UserID.GuestID(Auth.LocationId) : src.Detail.UserID, ToUserAll(index, src), Convert(index, src, maxTrackNo), onDone, onError) { } public PacketUpsertUserAll( int index, ulong userId, UserAll src, List<UserPlaylog> userPlaylogList, Action<int> onDone, Action<PacketStatus> onError = null) { // ... netQuery.Request.userId = userId; netQuery.Request.playlogId = loginVO?.loginId ?? NetDataManager.Instance.GetGuestLogId(index); netQuery.Request.isEventMode = GameManager.IsEventMode; netQuery.Request.isFreePlay = AmManager.Instance.Credit.IsFreePlay(); netQuery.Request.loginDateTime = loginVO?.loginDateTime ?? userPlaylogList[0].loginDate; netQuery.Request.userPlaylogList = userPlaylogList.ToArray(); netQuery.Request.upsertUserAll = src; // ... }
Net/Packet/Mai2/PacketUpsertUserAll.cs - 新增方法
1.52
private static UserAll ToUserAll(UserData src) { UserAll result = new UserAll(); src.ExportUserAll(ref result); return result; }
1.53
private static UserAll ToUserAll(int index, UserData src) { UserAll result = new UserAll(); src.ExportUserAll(index, ref result); return result; } private static List<UserPlaylog> Convert( int index, UserData src, int maxTrackNo) { List<UserPlaylog> list = new List<UserPlaylog>(); for (int i = 0; i < maxTrackNo; i++) { list.Add(src.ExportUserPlaylog(index, i)); } return list; }
⚙️ OperationManager - 运营管理器 Token/Cookie管理
Manager/OperationManager.cs - 新增using
1.52
using System; using AMDaemon; using AMDaemon.Allnet; using ChimeLib.NET; using IO; using MAI2.Util; using Manager.Operation; using Net; using Net.Packet; using Net.VO.Mai2; using UnityEngine;
1.53
using System; using System.Collections.Generic; using System.Net; using AMDaemon; using AMDaemon.Allnet; using ChimeLib.NET; using IO; using MAI2.Util; using Manager.Operation; using Net; using Net.Packet; using Net.VO.Mai2; using UnityEngine;
Manager/OperationManager.cs - 字段
1.52
public class OperationManager : Singleton<OperationManager> { private readonly Mode<OperationManager, State> _mode; private readonly OperationData _downloadData; private readonly OperationData _operationData; private readonly DataUploader _dataUploader; private readonly DataDownloaderMai2 _dataDownloader; private readonly MaintenanceTimer _maintenanceTimer; private readonly SegaBootTimer _segaBootTimer; private readonly ClosingTimer _closingTimer; // ... }
1.53
public class OperationManager : Singleton<OperationManager> { private readonly Mode<OperationManager, State> _mode; private readonly OperationData _downloadData; private readonly OperationData _operationData; private readonly DataUploader _dataUploader; private readonly DataDownloaderMai2 _dataDownloader; private readonly Dictionary<ulong, CookieContainer> _cookie; private readonly Dictionary<ulong, string> _token; private readonly MaintenanceTimer _maintenanceTimer; private readonly SegaBootTimer _segaBootTimer; private readonly ClosingTimer _closingTimer; // ... }
Manager/OperationManager.cs - 构造函数
1.52
public OperationManager() { this.IsAuthGood = false; this._mode = new Mode<OperationManager, State>(this); this._downloadData = new OperationData(); this._operationData = new OperationData(); this._dataUploader = new DataUploader(); this._dataDownloader = new DataDownloaderMai2(); this.ShopData = new ShopInfomation(); this._maintenanceTimer = new MaintenanceTimer(); this._closingTimer = new ClosingTimer(); this._segaBootTimer = new SegaBootTimer(); }
1.53
public OperationManager() { this.IsAuthGood = false; this._mode = new Mode<OperationManager, State>(this); this._downloadData = new OperationData(); this._operationData = new OperationData(); this._dataUploader = new DataUploader(); this._dataDownloader = new DataDownloaderMai2(); this.ShopData = new ShopInfomation(); this._cookie = new Dictionary<ulong, CookieContainer>(); this._token = new Dictionary<ulong, string>(); this._maintenanceTimer = new MaintenanceTimer(); this._closingTimer = new ClosingTimer(); this._segaBootTimer = new SegaBootTimer(); }
Manager/OperationManager.cs - 新增方法
1.52
// 无Cookie和Token管理方法
1.53
public void ResetCookie() { this._cookie.Clear(); } public void SetCookie(ulong userId, CookieContainer container) { if (this._cookie.ContainsKey(userId)) { this._cookie.Remove(userId); } this._cookie.Add(userId, container); } public CookieContainer GetCookie(ulong userId) { CookieContainer result; if (this._cookie.TryGetValue(userId, out result)) { return result; } return null; } public void ResetToken() { this._token.Clear(); } public void SetToken(ulong userId, string token) { if (this._token.ContainsKey(userId)) { this._token.Remove(userId); } this._token.Add(userId, token); } public string GetToken(ulong userId) { string result; if (this._token.TryGetValue(userId, out result)) { return result; } return ""; }
🗑️ 已删除的文件 功能移除
Net/Packet/Mai2/PacketUploadUserPlaylogList.cs - 已删除
1.52 (已删除)
public class PacketUploadUserPlaylogList : Packet { public PacketUploadUserPlaylogList( int index, UserData src, int maxTrackNo, Action<int> onDone, Action<PacketStatus> onError = null) public PacketUploadUserPlaylogList( ulong userId, List<UserPlaylog> userPlaylogList, Action<int> onDone, Action<PacketStatus> onError = null) { base.Create(new NetQuery< UserPlaylogListRequestVO, UpsertResponseVO>("UploadUserPlaylogListApi", userId) { Request = { userPlaylogList = userPlaylogList.ToArray() }, Request = { userId = userId } }, -1, -1, -1); } }
1.53
文件已删除 Playlog改为在UpsertUserAll中通过 userPlaylogList字段上传 API端点 "UploadUserPlaylogListApi" 已废弃
Net/VO/Mai2/UserPlaylogListRequestVO.cs - 已删除
1.52 (已删除)
[Serializable] public class UserPlaylogListRequestVO : VOSerializer { public ulong userId; public UserPlaylog[] userPlaylogList; }
1.53
文件已删除 userPlaylogList字段已移至UserAllRequestVO中

📊 技术总结

🔐 安全增强
• AES加密密钥和IV已更换 (32字节Key + 16字节IV)
• 新增Token认证机制 - 登录后服务器返回token
• 新增Cookie会话管理 - 使用CookieContainer
• UserPreview请求需要clientId验证机柜身份
🔄 API变更
• dateTime字段改为loginDateTime
• 登录响应新增token字段用于后续认证
• 登出方法需要传入index参数
• HTTP头Mai-Encoding从1.50更新为1.53
• UserPreview响应新增errorId字段
🗑️ 移除功能
• 移除独立的Playlog上传API
• 删除PacketUploadUserPlaylogList类
• 删除UserPlaylogListRequestVO类
• Playlog数据改为在UpsertUserAll中上传