2026-04-16 14:57:19 +08:00
|
|
|
|
using System;
|
2026-06-18 09:38:23 +08:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
|
using System.Text;
|
2026-04-16 14:57:19 +08:00
|
|
|
|
using System.Threading.Tasks;
|
2026-06-18 09:38:23 +08:00
|
|
|
|
using AppleAuth;
|
|
|
|
|
|
using AppleAuth.Enums;
|
|
|
|
|
|
using AppleAuth.Interfaces;
|
|
|
|
|
|
using AppleAuth.Native;
|
2026-04-16 14:57:19 +08:00
|
|
|
|
using Kill.Utils;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
2026-06-18 09:38:23 +08:00
|
|
|
|
#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR
|
2026-04-16 14:57:19 +08:00
|
|
|
|
using Firebase;
|
|
|
|
|
|
using Firebase.Auth;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
namespace Kill.Managers
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
2026-06-18 09:38:23 +08:00
|
|
|
|
/// Firebase 第三方登录管理器(支持 Google / Apple)
|
2026-04-16 14:57:19 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class FirebaseAuthManager : MonoBehaviour
|
|
|
|
|
|
{
|
|
|
|
|
|
public static FirebaseAuthManager Instance { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
// Google 登录配置 - 从 Firebase 控制台获取的 Web 客户端 ID
|
|
|
|
|
|
private const string WEB_CLIENT_ID = "785438724947-kpjbqi43hbj6eddianbjsgkgkkclkfmd.apps.googleusercontent.com";
|
|
|
|
|
|
|
2026-06-18 09:38:23 +08:00
|
|
|
|
#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR
|
2026-04-16 14:57:19 +08:00
|
|
|
|
private FirebaseAuth auth;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2026-06-18 09:38:23 +08:00
|
|
|
|
private AppleAuthManager appleAuthManager;
|
|
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
private void Awake()
|
|
|
|
|
|
{
|
|
|
|
|
|
Instance = this;
|
|
|
|
|
|
InitializeFirebase();
|
2026-06-18 09:38:23 +08:00
|
|
|
|
InitializeAppleAuth();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void Update()
|
|
|
|
|
|
{
|
|
|
|
|
|
appleAuthManager?.Update();
|
2026-04-16 14:57:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 初始化 Firebase
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void InitializeFirebase()
|
|
|
|
|
|
{
|
2026-06-18 09:38:23 +08:00
|
|
|
|
#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR
|
2026-04-16 14:57:19 +08:00
|
|
|
|
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var dependencyStatus = task.Result;
|
|
|
|
|
|
if (dependencyStatus == DependencyStatus.Available)
|
|
|
|
|
|
{
|
|
|
|
|
|
auth = FirebaseAuth.DefaultInstance;
|
|
|
|
|
|
Debug.Log("Firebase 初始化成功");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"Firebase 初始化失败: {dependencyStatus}");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
#else
|
2026-06-18 09:38:23 +08:00
|
|
|
|
Debug.Log("Firebase 第三方登录仅在 Android / iOS 平台可用");
|
2026-04-16 14:57:19 +08:00
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 09:38:23 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 初始化 Apple Sign In
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void InitializeAppleAuth()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (AppleAuthManager.IsCurrentPlatformSupported)
|
|
|
|
|
|
{
|
|
|
|
|
|
appleAuthManager = new AppleAuthManager(new PayloadDeserializer());
|
|
|
|
|
|
Debug.Log("Apple Sign In 初始化成功");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.Log("当前平台不支持 Apple Sign In");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 谷歌登录
|
|
|
|
|
|
/// </summary>
|
2026-04-20 08:31:41 +08:00
|
|
|
|
public void SignInWithGoogle(Action<GoogleLoginResult> onSuccess, Action<string> onError, int timeoutSeconds = 30)
|
2026-04-16 14:57:19 +08:00
|
|
|
|
{
|
2026-06-18 09:38:23 +08:00
|
|
|
|
SignInWithProvider("google.com", new string[] { "email", "profile" }, onSuccess, onError, timeoutSeconds);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 苹果登录(使用 apple-signin-unity + Firebase OAuth Credential)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void SignInWithApple(Action<GoogleLoginResult> onSuccess, Action<string> onError, int timeoutSeconds = 30)
|
|
|
|
|
|
{
|
|
|
|
|
|
#if UNITY_IOS && !UNITY_EDITOR
|
|
|
|
|
|
if (appleAuthManager == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
onError?.Invoke("Apple Sign In 未初始化");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!AppleAuthManager.IsCurrentPlatformSupported)
|
|
|
|
|
|
{
|
|
|
|
|
|
onError?.Invoke("当前平台不支持苹果登录");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var rawNonce = GenerateRandomString(32);
|
|
|
|
|
|
var nonce = GenerateSHA256NonceFromRawNonce(rawNonce);
|
|
|
|
|
|
|
|
|
|
|
|
var loginArgs = new AppleAuthLoginArgs(
|
|
|
|
|
|
LoginOptions.IncludeEmail | LoginOptions.IncludeFullName,
|
|
|
|
|
|
nonce);
|
|
|
|
|
|
|
|
|
|
|
|
appleAuthManager.LoginWithAppleId(
|
|
|
|
|
|
loginArgs,
|
|
|
|
|
|
credential =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var appleIdCredential = credential as IAppleIDCredential;
|
|
|
|
|
|
if (appleIdCredential == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
MainThread.Enqueue(() => onError?.Invoke("未获取到 Apple ID 凭证"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var identityToken = Encoding.UTF8.GetString(appleIdCredential.IdentityToken);
|
|
|
|
|
|
var authorizationCode = Encoding.UTF8.GetString(appleIdCredential.AuthorizationCode);
|
|
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(identityToken))
|
|
|
|
|
|
{
|
|
|
|
|
|
MainThread.Enqueue(() => onError?.Invoke("Apple identity token 为空"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
SignInWithAppleCredential(appleIdCredential, identityToken, authorizationCode, rawNonce, onSuccess, onError);
|
|
|
|
|
|
},
|
|
|
|
|
|
error =>
|
|
|
|
|
|
{
|
|
|
|
|
|
MainThread.Enqueue(() => onError?.Invoke($"Apple 登录失败: {error.LocalizedDescription}"));
|
|
|
|
|
|
});
|
|
|
|
|
|
#else
|
|
|
|
|
|
onError?.Invoke("苹果登录仅在 iOS 平台可用");
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#if UNITY_IOS && !UNITY_EDITOR
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 使用 Apple 凭证完成 Firebase 登录
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void SignInWithAppleCredential(
|
|
|
|
|
|
IAppleIDCredential appleIdCredential,
|
|
|
|
|
|
string identityToken,
|
|
|
|
|
|
string authorizationCode,
|
|
|
|
|
|
string rawNonce,
|
|
|
|
|
|
Action<GoogleLoginResult> onSuccess,
|
|
|
|
|
|
Action<string> onError)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (auth == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
onError?.Invoke("Firebase 未初始化");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var firebaseCredential = OAuthProvider.GetCredential(
|
|
|
|
|
|
"apple.com",
|
|
|
|
|
|
identityToken,
|
|
|
|
|
|
rawNonce,
|
|
|
|
|
|
authorizationCode);
|
|
|
|
|
|
|
|
|
|
|
|
auth.SignInWithCredentialAsync(firebaseCredential).ContinueWith(task =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (task.IsCanceled)
|
|
|
|
|
|
{
|
|
|
|
|
|
MainThread.Enqueue(() => onError?.Invoke("Firebase 登录被取消"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (task.IsFaulted)
|
|
|
|
|
|
{
|
|
|
|
|
|
MainThread.Enqueue(() => onError?.Invoke("Firebase 登录失败"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var user = auth.CurrentUser;
|
|
|
|
|
|
if (user == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
MainThread.Enqueue(() => onError?.Invoke("获取 Firebase 用户信息失败"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
MainThread.Enqueue(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = new GoogleLoginResult
|
|
|
|
|
|
{
|
|
|
|
|
|
FirebaseUserId = user.UserId,
|
|
|
|
|
|
Email = user.Email ?? appleIdCredential.Email,
|
|
|
|
|
|
DisplayName = user.DisplayName ?? GetAppleDisplayName(appleIdCredential.FullName),
|
|
|
|
|
|
PhotoUrl = user.PhotoUrl?.ToString(),
|
|
|
|
|
|
IdToken = ""
|
|
|
|
|
|
};
|
|
|
|
|
|
onSuccess?.Invoke(result);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 通用 OAuth 登录(Google)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void SignInWithProvider(string providerId, string[] scopes, Action<GoogleLoginResult> onSuccess, Action<string> onError, int timeoutSeconds)
|
|
|
|
|
|
{
|
2026-04-16 14:57:19 +08:00
|
|
|
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
|
|
|
|
|
if (auth == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
onError?.Invoke("Firebase 未初始化");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 08:31:41 +08:00
|
|
|
|
bool isCompleted = false;
|
|
|
|
|
|
|
|
|
|
|
|
// 启动超时检测
|
|
|
|
|
|
Task.Run(async () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(timeoutSeconds));
|
|
|
|
|
|
if (!isCompleted)
|
|
|
|
|
|
{
|
|
|
|
|
|
isCompleted = true;
|
2026-06-18 09:38:23 +08:00
|
|
|
|
MainThread.Enqueue(() => onError?.Invoke($"{providerId} 登录超时({timeoutSeconds}秒)"));
|
2026-04-20 08:31:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-18 09:38:23 +08:00
|
|
|
|
var provider = new FederatedOAuthProviderData(providerId);
|
|
|
|
|
|
provider.Scopes = scopes;
|
|
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
var federatedProvider = new FederatedOAuthProvider(provider);
|
|
|
|
|
|
|
|
|
|
|
|
auth.SignInWithProviderAsync(federatedProvider).ContinueWith(task =>
|
|
|
|
|
|
{
|
2026-04-20 08:31:41 +08:00
|
|
|
|
if (isCompleted) return; // 已超时,忽略结果
|
|
|
|
|
|
isCompleted = true;
|
|
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
if (task.IsFaulted)
|
|
|
|
|
|
{
|
2026-06-18 09:38:23 +08:00
|
|
|
|
MainThread.Enqueue(() => onError?.Invoke($"{providerId} 登录失败"));
|
2026-04-16 14:57:19 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-18 09:38:23 +08:00
|
|
|
|
|
2026-04-16 14:57:19 +08:00
|
|
|
|
if (task.IsCanceled)
|
|
|
|
|
|
{
|
2026-06-18 09:38:23 +08:00
|
|
|
|
MainThread.Enqueue(() => onError?.Invoke($"{providerId} 登录被取消"));
|
2026-04-16 14:57:19 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var user = auth.CurrentUser;
|
|
|
|
|
|
if (user == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
MainThread.Enqueue(() => onError?.Invoke("获取用户信息失败"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构造登录结果
|
|
|
|
|
|
MainThread.Enqueue(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = new GoogleLoginResult
|
|
|
|
|
|
{
|
|
|
|
|
|
FirebaseUserId = user.UserId,
|
|
|
|
|
|
Email = user.Email,
|
|
|
|
|
|
DisplayName = user.DisplayName,
|
|
|
|
|
|
PhotoUrl = user.PhotoUrl?.ToString(),
|
|
|
|
|
|
IdToken = ""
|
|
|
|
|
|
};
|
|
|
|
|
|
onSuccess?.Invoke(result);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
#else
|
2026-06-18 09:38:23 +08:00
|
|
|
|
// 编辑器 / iOS 下谷歌登录不可用
|
2026-04-16 14:57:19 +08:00
|
|
|
|
Task.Delay(1000).ContinueWith(_ =>
|
|
|
|
|
|
{
|
2026-06-18 09:38:23 +08:00
|
|
|
|
Debug.Log($"编辑器模拟 {providerId} 登录");
|
2026-04-16 14:57:19 +08:00
|
|
|
|
MainThread.Enqueue(() =>
|
|
|
|
|
|
{
|
2026-06-18 09:38:23 +08:00
|
|
|
|
onError?.Invoke($"{providerId} 登录在当前平台不可用");
|
2026-04-16 14:57:19 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-18 09:38:23 +08:00
|
|
|
|
/// 登出
|
2026-04-16 14:57:19 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void SignOut()
|
|
|
|
|
|
{
|
2026-06-18 09:38:23 +08:00
|
|
|
|
#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR
|
2026-04-16 14:57:19 +08:00
|
|
|
|
auth?.SignOut();
|
2026-06-18 09:38:23 +08:00
|
|
|
|
Debug.Log("Firebase 登出成功");
|
2026-04-16 14:57:19 +08:00
|
|
|
|
#endif
|
|
|
|
|
|
}
|
2026-06-18 09:38:23 +08:00
|
|
|
|
|
|
|
|
|
|
#region Nonce 生成
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 生成随机字符串作为 raw nonce
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static string GenerateRandomString(int length)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (length <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new Exception("Expected nonce to have positive length");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const string charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._";
|
|
|
|
|
|
using (var rng = new RNGCryptoServiceProvider())
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = new StringBuilder();
|
|
|
|
|
|
var randomNumberHolder = new byte[1];
|
|
|
|
|
|
|
|
|
|
|
|
while (result.Length < length)
|
|
|
|
|
|
{
|
|
|
|
|
|
var randomNumbers = new List<int>(16);
|
|
|
|
|
|
for (var i = 0; i < 16; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
rng.GetBytes(randomNumberHolder);
|
|
|
|
|
|
randomNumbers.Add(randomNumberHolder[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var randomNumber in randomNumbers)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (result.Length >= length)
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
if (randomNumber < charset.Length)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.Append(charset[randomNumber]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 计算 raw nonce 的 SHA256 值,传给 Apple
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static string GenerateSHA256NonceFromRawNonce(string rawNonce)
|
|
|
|
|
|
{
|
|
|
|
|
|
using (var sha = SHA256.Create())
|
|
|
|
|
|
{
|
|
|
|
|
|
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(rawNonce));
|
|
|
|
|
|
var result = new StringBuilder();
|
|
|
|
|
|
for (var i = 0; i < hash.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.Append(hash[i].ToString("x2"));
|
|
|
|
|
|
}
|
|
|
|
|
|
return result.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 辅助方法
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 从 Apple 全名构造显示名称
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static string GetAppleDisplayName(IPersonName fullName)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (fullName == null)
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
|
|
var parts = new List<string>();
|
|
|
|
|
|
if (!string.IsNullOrEmpty(fullName.GivenName))
|
|
|
|
|
|
parts.Add(fullName.GivenName);
|
|
|
|
|
|
if (!string.IsNullOrEmpty(fullName.FamilyName))
|
|
|
|
|
|
parts.Add(fullName.FamilyName);
|
|
|
|
|
|
|
|
|
|
|
|
return parts.Count > 0 ? string.Join(" ", parts) : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
2026-04-16 14:57:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-18 09:38:23 +08:00
|
|
|
|
/// 第三方登录结果(兼容 Google / Apple)
|
2026-04-16 14:57:19 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class GoogleLoginResult
|
|
|
|
|
|
{
|
|
|
|
|
|
public string FirebaseUserId; // Firebase 用户ID(用于绑定)
|
|
|
|
|
|
public string Email; // 邮箱
|
|
|
|
|
|
public string DisplayName; // 显示名称
|
|
|
|
|
|
public string PhotoUrl; // 头像URL
|
|
|
|
|
|
public string IdToken; // ID Token
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|