iOS独特用户标识符

93

我正在编写一个与我的服务器通过REST通信的iPhone应用程序。主要问题是,我需要以某种方式识别用户。不久前,我们可以使用UDID,但现在它已经不再允许使用了。那么我应该使用什么替代品呢?我需要一种iPhone上的标识符,这样用户就可以删除应用程序,再次安装它,他将获得相同的ID。


这是一个非常热门和时髦的话题。你先在这里搜索过类似的问题了吗?Google 呢?非常热门的话题... - Mark Granoff
有些人说我们应该在这种情况下使用MAC地址,但我不确定苹果的规定是否允许。 - Drabuna
苹果不允许这样做,因为它与设备的唯一标识符(UDID)一样具体。 - Zoleas
FYI,关于这个话题的谷歌搜索会导向“这个”帖子。 - optikradio
7个回答

187

我使用 CFUUIDCreate() 函数创建了一个 UUID:

+ (NSString *)GetUUID {
  CFUUIDRef theUUID = CFUUIDCreate(NULL);
  CFStringRef string = CFUUIDCreateString(NULL, theUUID);
  CFRelease(theUUID);
  return [(NSString *)string autorelease];
}

然后将上面的 UUID 设置为我的 NSString:

NSString *UUID = [nameofclasswhereGetUUIDclassmethodresides UUID];

我使用 SSKeyChain 将那个 UUID 存储到了 Keychain 中。

要使用 SSKeyChain 设置 UUID:

[SSKeychain setPassword:UUID forService:@"com.yourapp.yourcompany" account:@"user"];

获取它的方法:

NSString *retrieveuuid = [SSKeychain passwordForService:@"com.yourapp.yourcompany" account:@"user"];

当您将UUID设置为Keychain时,即使用户完全卸载并重新安装应用程序,它也会保持不变。

确保所有设备都在Keychain中具有相同的UUID。

  1. 设置您的应用程序使用iCloud。
  2. 将Keychain中的UUID也保存到NSUserDefaults中。
  3. 将NSUserDefaults中的UUID与键值数据存储一起传递到云端。
  4. 在首次运行应用程序时,检查云端数据是否可用,并在新设备上将UUID设置为Keychain。

现在您拥有一个持久且与所有设备共享/同步的唯一标识符。


1
+1 这是我见过的针对每个用户而不仅仅是设备的唯一 UUID 的最佳方法!非常感谢。 - d.ennis
非常好的简单解决方案。运作得很好。谢谢。 - Stefan Arn
1
我喜欢那比起奶酪更多。谢谢! - Matthew
@thejaz,我很困惑,因为你说你没有将UUID存储到iCloud,但是你又说你发送UUID到服务器进行合并。如果你没有存储它,那你从哪里获取UUID呢?你能详细说明一下你如何处理多个设备但同一用户的合并和检测吗? - user1218464
4
你好,提交至App Store时有任何问题吗?用户可以清空钥匙串吗? - Durgaprasad
显示剩余3条评论

70
首先,在iOS 5中,UDID已经过时。这并不意味着它已经消失了(尚未)。
其次,您应该问自己是否真的需要这样的东西。如果用户获得新设备并在其中安装您的应用程序会怎么样?同一用户,但UDID已更改。同时,原始用户可能已经出售了旧设备,因此现在完全新的用户安装了您的应用程序,并且基于UDID,您认为这是一个不同的人。
如果您不需要UDID,请使用CFUUIDCreate()创建唯一标识符,并在第一次启动时将其保存到用户默认设置中(先使用CFUUIDCreateString()将UUID转换为字符串)。它将在备份和还原中存活,甚至在原始用户切换到新设备时也会带来。在许多方面,它比UDID更好。
如果您确实需要唯一的设备标识符(听起来您并不需要),请使用Suhail的答案中指出的MAC地址。

1
CFUUIDCreate - 如果同一用户重新安装我的应用程序,它会保持不变还是会更改? - Drabuna
2
@Drabuna:不,CFUUIDCreate()每次调用都会创建一个新的UUID。 - Ole Begemann
1
用户默认设置会备份并且在下一次安装时保留,不是吗? - Tommy
1
@Tommy:是的。如果UUID已保存到备份中并从那里恢复,则它将幸存下来。 - Ole Begemann
1
一种更好的策略,特别是如果您正在尝试识别用户而不是设备,是运行CFUUIDCreate()一次,然后将结果存储在NSUbiquitousKeyValueStore中,以便它在所有具有相同iCloud帐户的iOS设备上同步,并在所有重新安装/设备重置时保留,无论用户是否使用了备份。 - Garrett
显示剩余5条评论

35

我正在更新我的应用程序,基于唯一标识符,支持iOS 4.3及以上版本。因此,

1)我无法使用[UIDevice currentDevice].uniqueIdentifier;,因为它已不再可用。

2)我无法使用[UIDevice currentDevice].identifierForVendor.UUIDString,因为它仅适用于 iOS 6.0 及以上版本,无法用于较低版本的 iOS。

3)MAC地址不是一个选项,因为在iOS-7中不允许使用。

4)OpenUDID 在一段时间前被弃用,并且在 iOS-6 中存在问题。

5)广告标识符对于 iOS-5 及以下版本也不可用。

最终我做了这些:

a) 将SFHFKeychainUtils添加到项目中。

b) 生成 CFUUID 键字符串。

 CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault);
    udidString = (NSString*)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuid));

c) 将其保存到Key Chain Utils中,否则它每次都会生成一个新的唯一值。

最终代码

+ (NSString *)GetDeviceID {
    NSString *udidString;
   udidString = [self objectForKey:@"deviceID"];
    if(!udidString)
    {
    CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault);
    udidString = (NSString*)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuid));
    CFRelease(cfuuid);
        [self setObject:udidString forKey:@"deviceID"];
    }
    return udidString;
}

+(void) setObject:(NSString*) object forKey:(NSString*) key
{
    NSString *objectString = object;
    NSError *error = nil;
    [SFHFKeychainUtils storeUsername:key
                         andPassword:objectString
                      forServiceName:@"LIB"
                      updateExisting:YES
                               error:&error];

    if(error)
        NSLog(@"%@", [error localizedDescription]);
}

+(NSString*) objectForKey:(NSString*) key
{
    NSError *error = nil;
    NSString *object = [SFHFKeychainUtils getPasswordForUsername:key
                                                  andServiceName:@"LIB"
                                                           error:&error];
    if(error)
        NSLog(@"%@", [error localizedDescription]);

    return object;
}

输入图像描述

更多详细信息


3
这个回答值得更多的赞同。 - Ratata Tata
2
讲得非常清楚明白。这肯定应该是被采纳的答案。 - user2043155
@OrlandoLeite 和 user2043155。谢谢你们。我本来以为这应该被认为是正确答案。:'( - Quamber Ali
@NSQuamber.java 对于这个问题被提出时所选择的答案是完全可以接受的,你不应该期望 OP 改变他们选择的答案只因为你给出了一个更加更新的答案。然而,我认为这个答案至少值得一个 +1,因为它是一个解释得很好的答案。 - Popeye

15

有些人想了解更多可用选项,如果你想了解,请查看@NSQuamber.java的答案。如果您想知道如何使用NSUUID并与iCloud同步,请继续阅读。这篇文章比我原来想象的要冗长,但我希望它能为那些正在执行这些步骤的人提供清晰的指导!

使用NSUUID

我使用NSUUID类来创建UUID:

NSUUID *uuid = [NSUUID UUID];

然后,要创建字符串,只需要调用 UUIDString 方法:

NSString *uuidString = [uuid UUIDString];

或者在一行里完成:

NSString *uuidString = [[NSUUID UUID] UUIDString];

我认为,这比尝试使用CFUUIDCreate并维护一个方法要容易得多。


编辑:我现在使用UICKeyChainStore

使用UICKeyChainStore设置UUID:

UICKeyChainStore *keychain = [UICKeyChainStore keyChainStoreWithService:@"com.sample.MyApp"];
keychain[@"com.sample.MyApp.user"] = userID;

要检索它:

UICKeyChainStore *keychain = [UICKeyChainStore keyChainStoreWithService:@"com.sample.MyApp"];
NSString *userID = keychain[@"com.sample.MyApp.user"];

我使用 SSKeyChain 将该 UUID 存储到 Keychain 中。

使用 SSKeyChain 设置 UUID 的方法:

[SSKeychain setPassword:userID forService:@"com.sample.MyApp.user" account:@"com.sample.MyApp"];

获取它的方法:

NSString *userID = [SSKeychain passwordForService:@"com.sample.MyApp.user" account:@"com.sample.MyApp"];

当您将UUID设置为Keychain时,即使用户完全卸载应用程序,然后重新安装,它也将持久存在。

与iCloud同步

因此,确保所有用户设备使用相同的UUID非常有用。这是为了确保数据在所有设备上同步,而不是每个设备都认为它是唯一的用户。

我的回答中有几个评论中关于如何进行同步的问题,所以现在我已经全部操作成功,我会提供更多细节。

配置iCloud/NSUbiquitousKeyValueStore使用

  1. 在Xcode的Project Navigator顶部单击项目。
  2. 选择Capabilities
  3. 打开iCloud。

现在它应该看起来像这样:Screenshot of iCloud enabled

使用NSUbiquitousKeyValueStore

使用iCloud相当简单。 要写入:

// create the UUID
NSUUID *userUUID = [[NSUUID UUID];
// convert to string
NSString *userID = [userUUID UUIDString];
// create the key to store the ID
NSString *userKey = @"com.sample.MyApp.user";

// Save to iCloud
[[NSUbiquitousKeyValueStore defaultStore] setString:userID forKey:userKey];

阅读:

// create the key to store the ID
NSString *userKey = @"com.sample.MyApp.user";

// read from iCloud
NSString *userID = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:userKey];

在编写NSUbiquitousKeyValueStore文档之前,必须先从iCloud读取。要强制读取,请调用以下方法:

[[NSUbiquitousKeyValueStore defaultStore] synchronize];

若要使您的应用程序接收iCloud中更改通知,请添加以下通知:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(iCloudStoreDidChange:)
                                             name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
                                           object:[NSUbiquitousKeyValueStore defaultStore]];

使用iCloud创建UUID

结合NSUUID、SSKeychain和NSUbiquityKeyValueStore,这是我生成用户ID的方法:

- (NSUUID *)createUserID {
    NSString *userKey = @"com.sample.MyApp.user";
    NSString *KEYCHAIN_ACCOUNT_IDENTIFIER = @"com.sample.MyApp";
    NSString *userID = [SSKeychain passwordForService:userKey account:KEYCHAIN_ACCOUNT_IDENTIFIER];
    if (userID) {
        return [[NSUUID UUID] initWithUUIDString:userID];
    }

    // check iCloud
    userID = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:userKey];
    if (!userID) {
        // none in iCloud, create one
        NSUUID *newUUID = [NSUUID UUID];
        userID = [newUUID UUIDString];
        // save to iCloud
        [[NSUbiquitousKeyValueStore defaultStore] setString:userID forKey:userKey];
    }

    // store the user ID locally
    [SSKeychain setPassword:userID forService:userKey account:KEYCHAIN_ACCOUNT_IDENTIFIER];
    return [[NSUUID UUID] initWithUUIDString:userID];
}

如何确保您的用户ID同步

由于向iCloud写入需要先下载iCloud中的任何数据,因此我将同步调用放在(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法的顶部。我还在那里添加了通知注册。这使我能够检测到来自iCloud的任何更改并适当处理它们。

以下是一个示例:

NSString *const USER_KEY = @"com.sample.MyApp.user";
NSString *const KEYCHAIN_ACCOUNT_IDENTIFIER = @"com.sample.MyApp";

- (void)iCloudStoreDidChange:(NSNotification *)notification {
    NSDictionary *userInfo = notification.userInfo;
    NSNumber *changeReason = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey];
    NSArray *keysChanged = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey];
    if (changeReason) {
        switch ([changeReason intValue]) {
            default:
            case NSUbiquitousKeyValueStoreServerChange:
            case NSUbiquitousKeyValueStoreInitialSyncChange:
                // check changed keys
                for (NSString *keyChanged in keysChanged) {
                    NSString *iCloudID = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:keyChanged];
                    if (![keyChanged isEqualToString:USER_KEY]) {
                        NSLog(@"Unknown key changed [%@:%@]", keyChanged, iCloudID);
                        continue;
                    }

                    // get the local key
                    NSString *localID = [SSKeychain passwordForService:keyChanged account:KEYCHAIN_ACCOUNT_IDENTIFIER];
                    if (!iCloudID) {
                        // no value from iCloud
                        continue;
                    }
                    // local ID not created yet
                    if (!localID) {
                        // save the iCloud value locally
                        [SSKeychain setPassword:iCloudID forService:keyChanged account:KEYCHAIN_ACCOUNT_IDENTIFIER];
                        continue; // continue because there is no user information on the server, so no migration
                    }

                    if ([iCloudID isEqualToString:localID]) {
                        // IDs match, so continue
                        continue;
                    }

                    [self handleMigration:keyChanged from:localID to:iCloudID];
                }

                break;
            case NSUbiquitousKeyValueStoreAccountChange:
                // need to delete all data and download new data from server
                break;
        }
    }
}

当应用程序启动或回到前台时,我会强制与iCloud同步并验证UUID的完整性。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [self configureSecKeyWrapper];
    // synchronize data from iCloud first. If the User ID already exists, then we can initialize with it
    [[NSUbiquitousKeyValueStore defaultStore] synchronize];
    [self checkUseriCloudSync];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    // synchronize changes from iCloud
    [[NSUbiquitousKeyValueStore defaultStore] synchronize];
    [self checkUseriCloudSync];
}

- (BOOL)checkUseriCloudSync {
    NSString *userKey = @"com.sample.MyApp.user";
    NSString *KEYCHAIN_ACCOUNT_IDENTIFIER = @"com.sample.MyApp";
    NSString *localID = [SSKeychain passwordForService:userKey account:KEYCHAIN_ACCOUNT_IDENTIFIER];
    NSString *iCloudID = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:userKey];

    if (!iCloudID) {
        // iCloud does not have the key saved, so we write the key to iCloud
        [[NSUbiquitousKeyValueStore defaultStore] setString:localID forKey:userKey];
        return YES;
    }

    if (!localID || [iCloudID isEqualToString:localID]) {
        return YES;
    }

    // both IDs exist, so we keep the one from iCloud since the functionality requires synchronization
    // before setting, so that means that it was the earliest one
    [self handleMigration:userKey from:localID to:iCloudID];
    return NO;
}

如果UUID的先后顺序很重要

在我的用户ID使用场景中,我认为iCloud中的值是需要保留的,因为它是第一个推送到iCloud中的UUID,不管哪个设备最先生成了UUID。大多数人可能会采取相同的做法,因为只要解析为单一的UUID,就不会关心它解析成哪一个。对于那些真正关心先后顺序的人,建议您存储UUID和时间戳生成 ([[NSDate date] timeIntervalSince1970]),这样可以检查哪一个更早:

// using dates
NSDate *uuid1Timestamp = [NSDate dateWithTimeIntervalSince1970:timestamp1];
NSDate *uuid2Timestamp = [NSDate dateWithTimeIntervalSince1970:timestamp2];
NSTimeInterval timeDifference = [uuid1 timeIntervalSinceDate:uuid2Timestamp];

// or just subtract
double timeDifference = timestamp1 - timestamp2;

@user1218464:我对我的答案做了一个大的编辑,展示了我如何处理同步。由于我在本地执行数据迁移并向服务器发出相同的请求,所以没有任何问题。 - mikeho
谢谢更新,非常有帮助。我还没有进行足够的测试,但在你回复之前,我尝试了一种稍微不同的方法,将钥匙串同步到iCloud,这样就不需要存储在NSUbiquitousKeyValueStore中。您还可以获取钥匙串密码创建的时间戳,以确保始终使用最旧的密码。我不确定它是否是最佳选项,但它肯定可以减少代码量。 - user1218464
如果在添加钥匙串项时设置了kSecAttrSynchronizable属性,则自iOS 7.0.3起,它将与iCloud同步。因此,我只需设置该属性,钥匙串项(UUID)就可以使用iCloud在设备之间同步。 - user1218464
你能否添加handleMigration方法的实现? - ipatch
@Chris 我可以这样做,但意义不大。 handleMigration 方法仅是为了将我的数据从一个UUID迁移到另一个UUID。实质上,我正在更改所有具有user字段的CoreData实体的所有权到新的UUID。然后,我将删除旧用户实体。然后将所有更改的对象推送到服务器以在服务器端进行修改。 - mikeho
显示剩余2条评论

10

谢谢Patel Ji,我想这是更好的选择,因为有些情况下我们希望特定的应用程序始终为该设备发出相同的UIID。 - infiniteLoop
1
MAC地址没有唯一性属性,无法替代UDID。请参见我的答案:https://dev59.com/PWbWa4cB1Zd3GeqPTyqR#16230057。 - james woodyatt
9
在iOS 7中,MAC地址现在不可用。 - MusiGenesis

1
在iOS7中,苹果公司在UIDevice类中引入了一个名为“identifierForVendor”的只读属性。如果您决定使用它,应注意以下几点:
  • 如果在用户解锁设备之前访问该值,则该值可能为空
  • 当用户从设备中删除所有该供应商的应用程序并随后重新安装其中一个或多个应用程序时,该值会更改。
  • 在使用Xcode安装测试版本或使用特定分发方式将应用程序安装到设备上时,该值也可能会更改。

如果您需要广告标识符,请使用ASIdentifierManager的advertisingIdentifier属性。但是请注意,上述第一点仍然适用。

来源:https://developer.apple.com/library/ios/documentation/uikit/reference/UIDevice_Class/Reference/UIDevice.html#//apple_ref/occ/instp/UIDevice/identifierForVendor


0

这确实是一个热门话题。我有一个应用程序需要迁移,因为它使用UDID来命名要存储在服务器上的XML文件。然后,带有该应用程序的设备将连接到服务器并下载其特定的udid.xml并解析它以使其工作。

我一直在思考,如果用户更换新设备,应用程序将会出现问题。所以我真的应该使用其他东西。问题是,我不使用数据库来存储数据。数据只是以xml格式存储,每个设备一个xml文件存储在云端。

我认为最好的方法是让用户在网页上填写数据,让php即时创建一个令牌,该令牌不会存储在数据库中,而是发送给用户。然后用户可以在目标设备上输入令牌并检索相关的xml。

这将是我解决问题的方法。不确定如何实现整个“创建唯一令牌”的过程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接