You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中如何借助共享Apple Keychain实现Firebase跨应用认证?

在Unity中借助共享Apple Keychain实现Firebase跨应用认证的方案

前提条件

  • 所有需要共享认证状态的App必须归属同一Apple开发者账号,且已在Apple开发者后台为各App ID配置相同的Keychain Group(格式通常为com.yourcompany.keychaingroup)
  • Unity项目已集成Firebase Auth SDK,且iOS平台基础配置完成
  • 使用的Xcode版本符合Firebase与Unity的最低要求

步骤1:配置iOS端Keychain共享

你可以选择手动处理导出的Xcode项目,或通过Unity的PostProcessBuild脚本自动配置:

方式1:手动配置Xcode项目

  1. 将Unity项目导出为Xcode工程并打开
  2. 进入项目的Signing & Capabilities标签,点击+ Capability添加Keychain Sharing
  3. 在Keychain Groups列表中添加你在开发者后台配置的Group ID

方式2:Unity自动配置脚本

在Unity项目的Assets/Editor目录下创建以下脚本,实现导出Xcode项目时自动添加Keychain共享能力:

using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using System.IO;

public class KeychainPostBuildHandler
{
    [PostProcessBuild(999)]
    public static void OnPostProcessBuild(BuildTarget target, string buildPath)
    {
        if (target != BuildTarget.iOS) return;

        // 编辑PBXProject
        string pbxPath = PBXProject.GetPBXProjectPath(buildPath);
        PBXProject pbxProject = new PBXProject();
        pbxProject.ReadFromFile(pbxPath);
        string targetGuid = pbxProject.GetUnityFrameworkTargetGuid();
        
        // 添加Keychain Sharing能力
        pbxProject.AddCapability(targetGuid, PBXCapabilityType.KeychainSharing);
        // 替换为你的Keychain Group ID
        pbxProject.AddKeychainGroup(targetGuid, "com.yourcompany.keychaingroup");
        
        pbxProject.WriteToFile(pbxPath);

        // 编辑Info.plist(添加必要权限说明)
        string plistPath = Path.Combine(buildPath, "Info.plist");
        PlistDocument plist = new PlistDocument();
        plist.ReadFromFile(plistPath);
        plist.root.SetString("NSUserTrackingUsageDescription", "用于跨应用共享Apple登录状态");
        plist.WriteToFile(plistPath);
    }
}

步骤2:实现Keychain凭证读写的原生桥接

在Unity项目的Assets/Plugins/iOS目录下创建以下Objective-C文件,提供Unity与iOS原生Keychain操作的接口:

#import <Foundation/Foundation.h>
#import <Security/Security.h>

@interface KeychainHelper : NSObject
+ (BOOL)saveAppleAuthData:(NSString *)userID identityToken:(NSString *)token toGroup:(NSString *)group;
+ (NSDictionary *)getAppleAuthDataFromGroup:(NSString *)group;
@end

@implementation KeychainHelper

+ (BOOL)saveAppleAuthData:(NSString *)userID identityToken:(NSString *)token toGroup:(NSString *)group {
    NSDictionary *query = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        (__bridge id)kSecAttrAccount: @"FirebaseAppleAuth",
        (__bridge id)kSecAttrService: @"CrossAppAuth",
        (__bridge id)kSecAttrAccessGroup: group,
        (__bridge id)kSecValueData: [token dataUsingEncoding:NSUTF8StringEncoding],
        (__bridge id)kSecAttrGeneric: [userID dataUsingEncoding:NSUTF8StringEncoding]
    };

    // 先删除旧凭证
    SecItemDelete((__bridge CFDictionaryRef)query);
    // 添加新凭证
    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
    return status == errSecSuccess;
}

+ (NSDictionary *)getAppleAuthDataFromGroup:(NSString *)group {
    NSDictionary *query = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        (__bridge id)kSecAttrAccount: @"FirebaseAppleAuth",
        (__bridge id)kSecAttrService: @"CrossAppAuth",
        (__bridge id)kSecAttrAccessGroup: group,
        (__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue,
        (__bridge id)kSecReturnAttributes: (__bridge id)kCFBooleanTrue,
        (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne
    };

    CFTypeRef result = NULL;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);

    if (status == errSecSuccess) {
        NSDictionary *item = (__bridge NSDictionary *)result;
        NSString *token = [[NSString alloc] initWithData:item[(__bridge id)kSecValueData] encoding:NSUTF8StringEncoding];
        NSString *userID = [[NSString alloc] initWithData:item[(__bridge id)kSecAttrGeneric] encoding:NSUTF8StringEncoding];
        CFRelease(result);
        return @{@"userID": userID, @"token": token};
    } else {
        CFRelease(result);
        return nil;
    }
}

@end

// Unity C#调用接口
extern "C" {
    bool _SaveAppleAuthData(const char* userID, const char* token, const char* group) {
        return [KeychainHelper saveAppleAuthData:[NSString stringWithUTF8String:userID] identityToken:[NSString stringWithUTF8String:token] toGroup:[NSString stringWithUTF8String:group]];
    }

    const char* _GetAppleAuthData(const char* group) {
        NSDictionary *data = [KeychainHelper getAppleAuthDataFromGroup:[NSString stringWithUTF8String:group]];
        if (data) {
            NSString *json = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:data options:0 error:nil] encoding:NSUTF8StringEncoding];
            return strdup([json UTF8String]);
        }
        return NULL;
    }
}

步骤3:Unity端Firebase认证逻辑

在Unity中编写C#脚本,实现Apple登录后的凭证存储,以及跨应用的Firebase自动登录:

using Firebase.Auth;
using System.Runtime.InteropServices;
using UnityEngine;
using System.Threading.Tasks;

public class CrossAppFirebaseAuth : MonoBehaviour
{
    [DllImport("__Internal")]
    private static extern bool _SaveAppleAuthData(string userID, string token, string group);

    [DllImport("__Internal")]
    private static extern string _GetAppleAuthData(string group);

    private const string KeychainGroup = "com.yourcompany.keychaingroup";

    // 第一个App中Apple登录成功后调用
    public async Task HandleAppleLoginSuccess(string appleUserID, string identityToken)
    {
        // 保存凭证到共享Keychain
        bool saveSuccess = _SaveAppleAuthData(appleUserID, identityToken, KeychainGroup);
        if (!saveSuccess)
        {
            Debug.LogError("Keychain凭证保存失败");
            return;
        }

        // 登录Firebase
        var credential = OAuthProvider.GetCredential("apple.com", identityToken);
        await FirebaseAuth.DefaultInstance.SignInWithCredentialAsync(credential);
        Debug.Log("Firebase登录成功");
    }

    // 第二个App启动时调用,尝试跨应用登录
    public async Task TryCrossAppLogin()
    {
        string credentialJson = _GetAppleAuthData(KeychainGroup);
        if (string.IsNullOrEmpty(credentialJson))
        {
            Debug.Log("Keychain中无可用认证凭证");
            return;
        }

        var authData = JsonUtility.FromJson<AppleAuthData>(credentialJson);
        var firebaseCredential = OAuthProvider.GetCredential("apple.com", authData.token);
        await FirebaseAuth.DefaultInstance.SignInWithCredentialAsync(firebaseCredential);
        Debug.Log("跨应用Firebase登录成功");
    }

    [System.Serializable]
    private class AppleAuthData
    {
        public string userID;
        public string token;
    }
}

关键注意事项

  • 所有App的Bundle ID必须在同一Apple开发者账号下,且Keychain Group完全一致
  • Apple登录仅支持iOS 13及以上版本,需处理低版本兼容逻辑
  • Firebase的OAuth凭证存在有效期,若登录失败需引导用户重新进行Apple登录
  • Keychain中存储的凭证建议加密处理,示例中使用的是Generic Password类型,可根据需求调整存储策略

内容的提问来源于stack exchange,提问作者AJS

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 20:40:57