328 lines
12 KiB
C#
328 lines
12 KiB
C#
using System;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using IchniOnline.Online.Network;
|
||
using IchniOnline.Online.Network.Models;
|
||
using TapSDK.Login;
|
||
using UnityEngine;
|
||
|
||
namespace IchniOnline.Online.Network.Models
|
||
{
|
||
[Serializable]
|
||
public class SessionKeyResponseDto
|
||
{
|
||
public string SessionKey;
|
||
public long ExpiresAt;
|
||
}
|
||
}
|
||
|
||
namespace IchniOnline.Online.Logic
|
||
{
|
||
/// <summary>
|
||
/// Authentication orchestration service that coordinates TapTap login,
|
||
/// password login, registration, and logout flows.
|
||
/// Pure orchestration — no UI state management. Results are delivered via events.
|
||
/// </summary>
|
||
public static class IchniOnlineAuthService
|
||
{
|
||
/// <summary>
|
||
/// 登录成功时触发,参数为服务端返回的登录响应(Register 成功时可能为 null)
|
||
/// </summary>
|
||
public static event Action<LoginResponseDto> OnLoginSuccess;
|
||
|
||
/// <summary>
|
||
/// 登录失败时触发,参数为错误信息
|
||
/// </summary>
|
||
public static event Action<string> OnLoginFailed;
|
||
|
||
/// <summary>
|
||
/// 登录被用户取消时触发
|
||
/// </summary>
|
||
public static event Action OnLoginCanceled;
|
||
|
||
/// <summary>
|
||
/// 是否正在进行登录流程,用于防止并发登录请求
|
||
/// </summary>
|
||
public static bool IsLoggingIn { get; private set; }
|
||
|
||
#region TapTap Login
|
||
|
||
/// <summary>
|
||
/// 使用 TapTap 登录的完整流程:
|
||
/// TapTap SDK → 第三方登录 API → JWT → 缓存 → 事件
|
||
/// </summary>
|
||
public static async void LoginWithTapTap()
|
||
{
|
||
if (IsLoggingIn)
|
||
{
|
||
Debug.LogWarning("[IchniOnlineAuthService] 已有登录流程正在进行中");
|
||
return;
|
||
}
|
||
|
||
IsLoggingIn = true;
|
||
|
||
Action<TapTapAccount> onSuccess = null;
|
||
Action onCanceled = null;
|
||
Action<string> onFailed = null;
|
||
|
||
onSuccess = async account =>
|
||
{
|
||
UnsubscribeTapTapEvents(onSuccess, onCanceled, onFailed);
|
||
|
||
if (account?.accessToken == null)
|
||
{
|
||
IsLoggingIn = false;
|
||
OnLoginFailed?.Invoke("TapTap 登录成功但 accessToken 为空");
|
||
return;
|
||
}
|
||
|
||
var dto = new ThirdPartyLoginRequestDto
|
||
{
|
||
Token = account.accessToken.kid,
|
||
TokenType = account.accessToken.tokenType,
|
||
MacKey = account.accessToken.macKey,
|
||
MacAlgorithm = account.accessToken.macAlgorithm
|
||
};
|
||
|
||
try
|
||
{
|
||
var result = await IchniOnlineApiClient.Instance.PostAsync<LoginResponseDto>("/api/auth/third-party/login", dto);
|
||
Debug.Log(JsonUtility.ToJson(result.Data));
|
||
IsLoggingIn = false;
|
||
|
||
if (result.IsSuccess)
|
||
{
|
||
LoginCacheManager.SaveAuthSession(result.Data.Token, result.Data);
|
||
IchniOnlineApiClient.Instance.JwtToken = result.Data.Token;
|
||
OnLoginSuccess?.Invoke(result.Data);
|
||
}
|
||
else
|
||
{
|
||
string errorMessage = $"第三方登录 API 失败: {result.Message}";
|
||
if (!string.IsNullOrEmpty(result.ErrorDetail))
|
||
errorMessage += $" ({result.ErrorDetail})";
|
||
OnLoginFailed?.Invoke(errorMessage);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
IsLoggingIn = false;
|
||
Debug.LogError($"[IchniOnlineAuthService] TapTap 登录 API 异常: {ex}");
|
||
OnLoginFailed?.Invoke($"TapTap 登录 API 异常: {ex.Message}");
|
||
}
|
||
};
|
||
|
||
onCanceled = () =>
|
||
{
|
||
UnsubscribeTapTapEvents(onSuccess, onCanceled, onFailed);
|
||
IsLoggingIn = false;
|
||
OnLoginCanceled?.Invoke();
|
||
};
|
||
|
||
onFailed = errorMessage =>
|
||
{
|
||
UnsubscribeTapTapEvents(onSuccess, onCanceled, onFailed);
|
||
IsLoggingIn = false;
|
||
OnLoginFailed?.Invoke($"TapTap 登录失败: {errorMessage}");
|
||
};
|
||
|
||
ThirdPartyServiceManager.Instance.OnLoginSuccess += onSuccess;
|
||
ThirdPartyServiceManager.Instance.OnLoginCanceled += onCanceled;
|
||
ThirdPartyServiceManager.Instance.OnLoginFailed += onFailed;
|
||
|
||
ThirdPartyServiceManager.Instance.StartTapTapLogin();
|
||
}
|
||
|
||
private static void UnsubscribeTapTapEvents(
|
||
Action<TapTapAccount> onSuccess,
|
||
Action onCanceled,
|
||
Action<string> onFailed)
|
||
{
|
||
if (ThirdPartyServiceManager.Instance != null)
|
||
{
|
||
ThirdPartyServiceManager.Instance.OnLoginSuccess -= onSuccess;
|
||
ThirdPartyServiceManager.Instance.OnLoginCanceled -= onCanceled;
|
||
ThirdPartyServiceManager.Instance.OnLoginFailed -= onFailed;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Password Login
|
||
|
||
/// <summary>
|
||
/// 使用用户名和密码登录的完整流程:
|
||
/// 获取 session-key → XOR 加密密码 → 登录 API → JWT → 缓存 → 事件
|
||
/// </summary>
|
||
public static async void LoginWithPassword(string username, string password)
|
||
{
|
||
if (IsLoggingIn)
|
||
{
|
||
Debug.LogWarning("[IchniOnlineAuthService] 已有登录流程正在进行中");
|
||
return;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
||
{
|
||
OnLoginFailed?.Invoke("用户名或密码不能为空");
|
||
return;
|
||
}
|
||
|
||
IsLoggingIn = true;
|
||
|
||
try
|
||
{
|
||
// 1. 获取 session-key
|
||
var sessionResult = await IchniOnlineApiClient.Instance.GetAsync<SessionKeyResponseDto>("/api/auth/session-key");
|
||
if (!sessionResult.IsSuccess)
|
||
{
|
||
IsLoggingIn = false;
|
||
string errorMessage = $"获取 session-key 失败: {sessionResult.Message}";
|
||
if (!string.IsNullOrEmpty(sessionResult.ErrorDetail))
|
||
errorMessage += $" ({sessionResult.ErrorDetail})";
|
||
OnLoginFailed?.Invoke(errorMessage);
|
||
return;
|
||
}
|
||
|
||
string sessionKey = sessionResult.Data.SessionKey;
|
||
|
||
// 2. XOR 加密密码
|
||
string encryptedPassword = EncryptPassword(password, sessionKey);
|
||
|
||
// 3. 调用登录 API
|
||
var loginDto = new LoginRequestDto
|
||
{
|
||
Username = username,
|
||
EncryptedPassword = encryptedPassword,
|
||
SessionKey = sessionKey
|
||
};
|
||
|
||
var loginResult = await IchniOnlineApiClient.Instance.PostAsync<LoginResponseDto>("/api/auth/login", loginDto);
|
||
|
||
IsLoggingIn = false;
|
||
|
||
if (loginResult.IsSuccess)
|
||
{
|
||
LoginCacheManager.SaveAuthSession(loginResult.Data.Token, loginResult.Data);
|
||
IchniOnlineApiClient.Instance.JwtToken = loginResult.Data.Token;
|
||
OnLoginSuccess?.Invoke(loginResult.Data);
|
||
}
|
||
else
|
||
{
|
||
string errorMessage = $"登录失败: {loginResult.Message}";
|
||
if (!string.IsNullOrEmpty(loginResult.ErrorDetail))
|
||
errorMessage += $" ({loginResult.ErrorDetail})";
|
||
OnLoginFailed?.Invoke(errorMessage);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
IsLoggingIn = false;
|
||
Debug.LogError($"[IchniOnlineAuthService] 密码登录异常: {ex}");
|
||
OnLoginFailed?.Invoke($"登录异常: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Register
|
||
|
||
/// <summary>
|
||
/// 用户注册流程:POST /api/auth/register
|
||
/// 注册成功后触发 OnLoginSuccess(null)(注册不返回 JWT)
|
||
/// </summary>
|
||
public static async void Register(string username, string password, string displayName)
|
||
{
|
||
if (IsLoggingIn)
|
||
{
|
||
Debug.LogWarning("[IchniOnlineAuthService] 已有登录流程正在进行中");
|
||
return;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
||
{
|
||
OnLoginFailed?.Invoke("用户名或密码不能为空");
|
||
return;
|
||
}
|
||
|
||
IsLoggingIn = true;
|
||
|
||
try
|
||
{
|
||
var registerDto = new RegisterRequestDto
|
||
{
|
||
Username = username,
|
||
Password = password,
|
||
DisplayName = displayName
|
||
};
|
||
|
||
var result = await IchniOnlineApiClient.Instance.PostAsync<object>("/api/auth/register", registerDto);
|
||
|
||
IsLoggingIn = false;
|
||
|
||
if (result.IsSuccess)
|
||
{
|
||
OnLoginSuccess?.Invoke(null);
|
||
}
|
||
else
|
||
{
|
||
string errorMessage = $"注册失败: {result.Message}";
|
||
if (!string.IsNullOrEmpty(result.ErrorDetail))
|
||
errorMessage += $" ({result.ErrorDetail})";
|
||
OnLoginFailed?.Invoke(errorMessage);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
IsLoggingIn = false;
|
||
Debug.LogError($"[IchniOnlineAuthService] 注册异常: {ex}");
|
||
OnLoginFailed?.Invoke($"注册异常: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Logout
|
||
|
||
/// <summary>
|
||
/// 登出流程:清除本地会话、登出 TapTap、清除 API JWT Token
|
||
/// </summary>
|
||
public static void Logout()
|
||
{
|
||
LoginCacheManager.ClearSession();
|
||
ThirdPartyServiceManager.Instance?.Logout();
|
||
IchniOnlineApiClient.Instance.JwtToken = null;
|
||
Debug.Log("[IchniOnlineAuthService] 已登出");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Encryption
|
||
|
||
/// <summary>
|
||
/// 使用 XOR 算法加密密码,与服务器 UserService.DecryptPassword 对应。
|
||
/// sessionKey 为服务器返回的 Base64 字符串。
|
||
/// </summary>
|
||
public static string EncryptPassword(string password, string sessionKey)
|
||
{
|
||
if (string.IsNullOrEmpty(password))
|
||
throw new ArgumentException("密码不能为空", nameof(password));
|
||
if (string.IsNullOrEmpty(sessionKey))
|
||
throw new ArgumentException("sessionKey 不能为空", nameof(sessionKey));
|
||
|
||
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
|
||
byte[] sessionBytes = Convert.FromBase64String(sessionKey);
|
||
byte[] encrypted = new byte[passwordBytes.Length];
|
||
|
||
for (int i = 0; i < passwordBytes.Length; i++)
|
||
{
|
||
encrypted[i] = (byte)(passwordBytes[i] ^ sessionBytes[i % sessionBytes.Length]);
|
||
}
|
||
|
||
return Convert.ToBase64String(encrypted);
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|