Files
ichni_Official/Assets/Scripts/Online/Logic/AuthService.cs
2026-06-15 18:18:16 +08:00

328 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}