如何在Swift中获取真正的固定设备ID?

5

我使用以下代码已经很长时间了。我一直认为它很独特。

但是,当我删除我的应用并重新安装它时,我得到了新的不同的设备标识。

if let uuid = UIDevice.current.identifierForVendor?.uuidString {
  print(uuid)
}

每次重新安装后,我都会得到一个新的ID。

我该如何获得一个保持不变的ID?


不要使用特定于设备ID的任务,而是选择像用户ID +设备ID(identifierForVendor)这样的方式,可以用作处理任何情况的关键。在推送通知相关工作中,我更喜欢选择这种方式。否则,如果您想要唯一的设备ID,则可以使用第三方库提供的唯一ID,否则您需要编写自己的逻辑来生成唯一ID。 - Shobhakar Tiwari
3个回答

32

由于从identifierForVendor返回的值在删除应用程序时可能会被清除,或者如果用户在“设置”应用程序中重置它,则会被重置,因此必须自己管理其持久化。

有几种方法可以实现这一点。您可以设置一个服务器,分配一个UUID,然后通过用户登录在服务器端持久保存和获取它,或者您可以在钥匙串中创建并本地存储它。

存储在钥匙串中的项不会在删除应用程序时被删除。 这使您可以检查以前是否存储了UUID,如果是,则可以检索它,如果没有,则可以生成新的UUID并将其持久化。

以下是您可以在本地执行此操作的方法:

/// Creates a new unique user identifier or retrieves the last one created
func getUUID() -> String? {

    // create a keychain helper instance
    let keychain = KeychainAccess()

    // this is the key we'll use to store the uuid in the keychain
    let uuidKey = "com.myorg.myappid.unique_uuid"

    // check if we already have a uuid stored, if so return it
    if let uuid = try? keychain.queryKeychainData(itemKey: uuidKey), uuid != nil {
        return uuid
    }

    // generate a new id
    guard let newId = UIDevice.current.identifierForVendor?.uuidString else {
        return nil
    }

    // store new identifier in keychain
    try? keychain.addKeychainData(itemKey: uuidKey, itemValue: newId)

    // return new id
    return newId
}

这是用于存储/检索钥匙串的类:

import Foundation

class KeychainAccess {

    func addKeychainData(itemKey: String, itemValue: String) throws {
        guard let valueData = itemValue.data(using: .utf8) else {
            print("Keychain: Unable to store data, invalid input - key: \(itemKey), value: \(itemValue)")
            return
        }

        //delete old value if stored first
        do {
            try deleteKeychainData(itemKey: itemKey)
        } catch {
            print("Keychain: nothing to delete...")
        }

        let queryAdd: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: itemKey as AnyObject,
            kSecValueData as String: valueData as AnyObject,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
        ]
        let resultCode: OSStatus = SecItemAdd(queryAdd as CFDictionary, nil)

        if resultCode != 0 {
            print("Keychain: value not added - Error: \(resultCode)")
        } else {
            print("Keychain: value added successfully")
        }
    }

    func deleteKeychainData(itemKey: String) throws {
        let queryDelete: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: itemKey as AnyObject
        ]

        let resultCodeDelete = SecItemDelete(queryDelete as CFDictionary)

        if resultCodeDelete != 0 {
            print("Keychain: unable to delete from keychain: \(resultCodeDelete)")
        } else {
            print("Keychain: successfully deleted item")
        }
    }

    func queryKeychainData (itemKey: String) throws -> String? {
        let queryLoad: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: itemKey as AnyObject,
            kSecReturnData as String: kCFBooleanTrue,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var result: AnyObject?
        let resultCodeLoad = withUnsafeMutablePointer(to: &result) {
            SecItemCopyMatching(queryLoad as CFDictionary, UnsafeMutablePointer($0))
        }

        if resultCodeLoad != 0 {
            print("Keychain: unable to load data - \(resultCodeLoad)")
            return nil
        }

        guard let resultVal = result as? NSData, let keyValue = NSString(data: resultVal as Data, encoding: String.Encoding.utf8.rawValue) as String? else {
            print("Keychain: error parsing keychain result - \(resultCodeLoad)")
            return nil
        }
        return keyValue
    }
}

那么你可以创建一个用户类,从中获取标识符:

let uuid = getUUID()
print("UUID: \(uuid)") 

如果你把这段代码放在viewDidLoad方法中,并启动应用程序并注意控制台中打印的uuid,然后删除应用程序并重新启动,你将得到相同的uuid。

如果你喜欢的话,也可以在应用程序中创建自己完全定制的uuid,例如执行以下操作:

// convenience extension for creating an MD5 hash from a string
extension String {

    func MD5() -> Data? {
        guard let messageData = data(using: .utf8) else { return nil }

        var digestData = Data(count: Int(CC_MD5_DIGEST_LENGTH))
        _ = digestData.withUnsafeMutableBytes { digestBytes in
            messageData.withUnsafeBytes { messageBytes in
                CC_MD5(messageBytes, CC_LONG(messageData.count), digestBytes)
            }
        }

        return digestData
    }
}

// extension on UUID to generate your own custom UUID
extension UUID {

    static func custom() -> String? {
        guard let bundleID = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String else {
            return nil
        }

        let unique = bundleID + NSUUID().uuidString
        let hashData = unique.MD5()
        let md5String = hashData?.map { String(format: "%02hhx", $0) }.joined()

        return md5String
    }
}

请注意,在使用MD5函数之前,您需要将以下导入内容添加到应用程序中的Objective-C桥接标头中:(如果您正在使用Xcode<10构建。在Xcode 10+中,CommonCrypto已包含,因此您可以跳过此步骤)

#import <CommonCrypto/CommonCrypto.h>
如果您的应用程序没有桥接头文件,请将其添加到您的项目中,并确保在构建设置中设置它: Screenshot 设置完成后,您可以像这样生成自己的自定义UUID:
let otherUuid = UUID.custom()
print("Other: \(otherUuid)")

运行应用程序并记录两个输出,将生成类似于以下内容的UUID:

结果为:

运行应用程序并记录两个输出,将生成类似于以下内容的UUID:

// uuid from first example
UUID: Optional("8A2496F0-EFD0-4723-8C6D-8E18431A49D2")

// uuid from second custom example
Other: Optional("63674d91f08ec3aaa710f3448dd87818")

1
卸载应用时,钥匙串数据是否仍然保留?根据https://forums.developer.apple.com/message/210531#210531,在iOS 10.3中发生了变化。 - Gereon
1
@Gereon 在 iPhone X 模拟器上测试 iOS 11.4,看起来它仍然可以工作。在我的设备上运行 11.4 也可以正常工作。 - digitalHound
谢谢,这真的很有趣! - Gereon
这更多是一个主观的业务逻辑问题,一切都取决于你想做什么。如果用户尝试使用多个设备登录,则可能会出现这种情况,这并不一定意味着他们已删除应用程序。我可能会给他们提供更新注册设备并用新设备替换旧设备ID的选项。如果您想锁定它,那么可以通过让他们在您发送到系统上注册的电子邮件地址中点击链接来完成,该链接使用深度链接并启动应用程序以将更新的UUID发送到您的服务器。 - digitalHound
我认为当一个用户可以使用多个iOS设备时,它可能不起作用。 - famfamfam
显示剩余3条评论

2

在iPhone中,唯一的ID是UDID,但在当前操作系统版本中无法访问它,因为它可能会被滥用。因此,苹果公司提供了其他选项作为唯一标识符,但每次安装应用程序时都会更改。

--无法访问UDID

但是还有另一种实现此功能的方法。

首先,您需要生成唯一ID:

func createUniqueID() -> String {
    let uuid: CFUUID = CFUUIDCreate(nil)
    let cfStr: CFString = CFUUIDCreateString(nil, uuid)

    let swiftString: String = cfStr as String
    return swiftString
}

在应用程序安装和重新安装后,获取此唯一标识符的值会发生更改。将此ID保存到任何密钥上的Key-Chain中,例如“uniqueID”。
要将密钥保存在KeyChain中:
   func getDataFromKeyChainFunction() {
        let uniqueID = KeyChain.createUniqueID()
        let data = uniqueID.data(using: String.Encoding.utf8)
        let status = KeyChain.save(key: "uniqueID", data: data!)
        if let udid = KeyChain.load(key: "uniqueID") {
            let uniqueID = String(data: udid, encoding: String.Encoding.utf8)
            print(uniqueID!)
        }
    }

func save(key: String, data: Data) -> OSStatus {
        let query = [
            kSecClass as String       : kSecClassGenericPassword as String,
            kSecAttrAccount as String : key,
            kSecValueData as String   : data ] as [String : Any]    
        SecItemDelete(query as CFDictionary)    
        return SecItemAdd(query as CFDictionary, nil)
}

如果您需要基于唯一标识符执行任何任务,首先请检查关键字“uniqueID”是否保存在密钥串中。

即使您卸载应用程序,密钥串数据仍然会保留,并将由操作系统删除。

func checkUniqueID() {
    if let udid = KeyChain.load(key: "uniqueID") {
        let uniqueID = String(data: udid, encoding: String.Encoding.utf8)
        print(uniqueID!)
    } else {
        let uniqueID = KeyChain.createUniqueID()
        let data = uniqueID.data(using: String.Encoding.utf8)
        let status = KeyChain.save(key: "uniqueID", data: data!)
        print("status: ", status)
    }
}

通过这种方式,您可以生成唯一ID,并在每次使用时使用此ID。

注意:但是,当您上传应用程序的下一个版本时,请使用相同的配置文件上传,否则无法访问上一个安装的应用程序的密钥链存储。 密钥链存储与配置文件相关联。

点击此处查看


1

访问唯一设备标识符(UDID)已被禁止很长时间了。 identifierForVendor 是它的替代品,其行为始终有文档记录。


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