在 React Native 上使用设备标识符(Ping Identity Orchestration SDK) 原文标题:Using device identifiers on React Native
内容概要总结
本文是 Ping Identity Orchestration SDK 官方文档,讲解在 React Native 中如何使用设备标识符。标识符在 SDK 中有两种角色:嵌入设备画像载荷(collectDeviceProfile 时发送给服务器),以及 bindForJourney() 注册设备时生成的每用户密钥标识符(kid)。默认实现委托原生层:iOS 用 Keychain 支撑的 RSA 密钥对(公钥 SHA-256),Android 用 Android KeyStore 的 RSA 密钥对(公钥 SHA-256)。文档对比两平台持久化差异——iOS 标识符可跨重装保留、可从加密备份恢复,并可通过 Keychain Access Group 在同一开发者的 App 间共享;Android 标识符在重装、清除数据后重新生成,标准备份不恢复。Android 另提供 AndroidIDDeviceIdentifier(Settings.Secure.ANDROID_ID,API 26+ 跨重装稳定)与 LegacyDeviceIdentifier 两种内置策略。文档附日志、自定义 Keychain 账户、regenerateIdentifier()、UUID 回退、自定义 DeviceIdentifier 实现及向 JS 暴露标识符的代码示例。
翻译内容
原文内容(English)
Orchestration SDKs
在 React Native 上使用设备标识符
PingOne Advanced Identity Cloud | PingAM | React Native
设备画像(Device Profiling)模块会在其采集的画像中包含一个设备标识符。在 Android 和 iOS 上,原生 SDK 都使用由 Device ID 模块生成的同一个安全标识符。
在 React Native 中,你不需要调用 @ping-identity/rn-device-id。当你调用 collectDeviceProfile 时,该标识符会由原生层自动解析。不过,如果你需要独立地读取同一个标识符(用于日志或展示),可以直接使用来自 @ping-identity/rn-device-id 的 getDeviceId()。
设备标识符在 SDK 中承担两种不同的角色:
- 当处理 DeviceProfileCallback、PingOneProtectInitializeCallback 或 PingOneProtectEvaluationCallback 时,会有一个唯一标识符被嵌入到发送给服务器的设备画像载荷(device profile payload)中。
- 当 bindForJourney() 注册设备时,会生成一个每用户的密钥标识符(kid,key identifier),并与加密密钥对一起存储。
这两种角色都在底层原生平台 SDK 中实现。React Native 模块将标识符的生成与存储委托给这些原生实现。理解平台默认行为——以及知道何时、如何自定义它们——有助于你控制标识符的稳定性、跨 App 共享以及恢复行为。
设备标识符如何工作
React Native 的 Orchestration SDK 封装了原生 Android 和 iOS 实现。设备标识符的生成完全发生在原生代码中;JavaScript 层会将标识符作为画像或绑定载荷的一部分透明地传递。
下表给出平台对比摘要。
| 特性 | iOS | Android |
|---|---|---|
| 默认标识符来源 | 由 Keychain 支撑的 RSA 密钥对(公钥的 SHA-256) | Android KeyStore RSA 密钥对(公钥的 SHA-256) |
| 跨 App 重装是否保留 | 否——重装时重新生成 | |
| 跨设备恢复是否保留 | 是(来自加密备份) | |
| 可从 JavaScript 配置 | 否——在原生 Xcode 工程中配置 | 否——在原生 Android 工程中配置 |
| 备用的内置策略 | AndroidIDDeviceIdentifier、LegacyDeviceIdentifier | |
| 自定义标识符支持 | 是——实现 DeviceIdentifier 协议 | 是——实现 DeviceIdentifier 接口 |
| 是——通过 Keychain Access Group | 是——通过 AndroidIDDeviceIdentifier(API 26+) |
iOS 默认值
在 iOS 上,默认的设备标识符是 Keychain 中生成并存储的公钥的 SHA-256 哈希。Keychain 在 App 重装后依然保留(除非设备被擦除),这赋予标识符很强的稳定性。
iOS 上默认标识符的持久化行为
| 场景 | 行为 |
|---|---|
| App 卸载并重装 | 标识符保留。删除 App 时 iOS Keychain 不会被清除,因此重装后的 App 会取回同一把密钥。 |
| 这在 iOS 上并非标准用户操作。App 重装是最接近的等价场景,如上所述会保留标识符。 | |
| 设备备份与恢复 | 如果从加密的 iCloud 或本地备份恢复,标识符会保留,因为这些备份包含 Keychain 数据。 |
| 标识符被永久删除。恢复出厂设置会清除所有设备存储,包括 Keychain。 | |
| 跨 App 共享(同一开发者) | 同一开发者的 App 之间可以通过 Keychain Access Group 共享该标识符。在原生 iOS SDK 中配置 keychainAccount 属性。 |
Android 默认值
在 Android 上,默认的设备标识符使用 Android KeyStore 生成并存储一个 RSA 密钥对。公钥的 SHA-256 哈希即为该标识符。
Android 上默认标识符的持久化行为
| 场景 | 行为 |
|---|---|
| App 卸载并重装 | 标识符会重新生成。Android KeyStore 条目以 App 的 UID 为作用域,而 UID 在重装时会改变。 |
| App 数据被清除(设置 > 清除数据) | 标识符会重新生成。清除 App 数据会移除 KeyStore 条目。 |
| 设备备份与恢复 | 标识符不会被恢复。Android KeyStore 条目不包含在标准备份中。 |
| 标识符被永久删除。 | |
| 默认的 KeyStore 策略不支持(跨 App 共享)。如果需要在 API 26+ 上实现跨 App 共享,请改用 AndroidIDDeviceIdentifier(ANDROID_ID)。 |
iOS(重装后仍保留)与 Android(重装后重新生成)之间的持久化差异,对服务器端逻辑很重要。如果你的 Protect 策略或设备管理规则依赖于标识符的稳定性,请针对稳定性较弱的 Android 行为来设计,以确保跨平台的一致性。
配置设备标识符
React Native SDK 中的设备标识符行为由底层的原生平台模块控制。JavaScript 侧的配置选项仅限于 JS 桥接边界上可见的关注点——主要是日志。原生级别的自定义(标识符策略、Keychain 账户、KeyStore 参数)需要编辑原生工程文件。
为标识符诊断挂载 logger
collectDeviceProfile 和 createBindingClient 都接受一个 LoggerInstance。
挂载 logger 可以让你看到标识符的生成与读取操作:
import { collectDeviceProfile } from '@ping-identity/rn-device-profile';
import { createBindingClient } from '@ping-identity/rn-binding';
import { logger } from '@ping-identity/rn-logger';
const sdkLogger = logger({ level: 'debug' });
// Device profiling — logger shows native identifier resolution
const profile = await collectDeviceProfile(['platform', 'hardware', 'network'], {
logger: sdkLogger,
});
// Device binding — logger shows key generation and identifier storage
const bindingClient = createBindingClient({
logger: sdkLogger,
});
在 iOS 上自定义默认标识符
iOS SDK 的 DefaultDeviceIdentifier 使用由 Keychain 支撑的 RSA 密钥对。要自定义其配置——例如通过 Keychain Access Group 在 App 间共享它、更改账户名或调整密钥长度——请在你的 Xcode 工程中编辑原生 iOS 模块的初始化代码。
自定义 Keychain 账户
默认情况下,SDK 会将标识符存储在一个由你的 bundle ID 派生出的 Keychain 账户下。
要在同一开发者的 App 之间共享该标识符,请配置一个共享的 Keychain Access Group:
// In your iOS AppDelegate or a native module initializer
import PingDeviceId
let config = DeviceIdentifierConfiguration(
keychainAccount: "com.example.shared.deviceid", // shared across your apps
useEncryption: true,
keySize: 2048
)
let deviceIdentifier = try DefaultDeviceIdentifier(configuration: config)
在 React Native 桥接初始化这些模块之前,把这个已配置的实例传给原生 SDK。
生产环境中不要禁用加密(useEncryption: false)。
禁用加密会移除由 Keychain 支撑的标识符所具备的安全保证。
重新生成标识符
要强制生成一个新的标识符——例如在安全事件之后,或当用户明确要求移除设备时——在原生代码中调用 regenerateIdentifier():
// In a native module exposed to React Native
import PingDeviceId
func resetDeviceIdentifier() async throws -> String {
let deviceIdentifier = try DefaultDeviceIdentifier()
let newId = try await deviceIdentifier.regenerateIdentifier()
print("New Device ID: \(newId)")
return newId
}
回退到基于 UUID 的标识符
如果某些设备上 Keychain 操作失败(例如对 Keychain 访问受限的共享企业设备),你可以回退到更简单的基于 UUID 的标识符:
import PingDeviceId
// UUIDDeviceIdentifier stores a UUID in UserDefaults rather than the Keychain
let deviceIdentifier = try UUIDDeviceIdentifier()
let id = try await deviceIdentifier.id
UUIDDeviceIdentifier 比默认的 Keychain 支撑方案稳定性更弱:当 App 数据被删除时,UUID 会被清除。只在 Keychain 访问不可靠时才使用它。
创建自定义 iOS 标识符
对于高级用例,实现 DeviceIdentifier 协议以提供一个完全自定义的标识符:
import PingDeviceId
struct CustomDeviceIdentifier: DeviceIdentifier {
var id: String {
get async throws {
// Return your custom stable identifier.
// Ensure persistence — return the same value across app launches.
return try await loadOrGenerateStableId()
}
}
}
在 App 开始处理 journey 之前,将自定义实现注册到设备画像或绑定模块。
在 Android 上自定义默认标识符
Android SDK 提供三种内置的标识符策略,并支持自定义实现。
Android 内置标识符策略
| 策略 | 重装后是否持久 | 说明 |
|---|---|---|
| DefaultDeviceIdentifier | 使用 Android KeyStore。最安全。未指定策略时的默认值。 | |
| AndroidIDDeviceIdentifier | 使用 Settings.Secure.ANDROID_ID。在 Android 8.0+ 上跨重装保留,因为该值以签名证书和用户账户为作用域。恢复出厂设置时重置。 | |
| LegacyDeviceIdentifier | 向后兼容策略,供从旧版 SDK 迁移的 App 使用。仅在需要与迁移前的标识符保持连续性时使用。 |
切换到 AndroidIDDeviceIdentifier
要使用 ANDROID_ID 作为设备标识符——例如在重装后维持一个稳定的标识符——请配置原生 Android 模块:
// In your Android Application subclass or native module setup
import com.pingidentity.deviceprofile.DefaultDeviceIdentifier
import com.pingidentity.deviceprofile.AndroidIDDeviceIdentifier
// Replace the default strategy with ANDROID_ID
val deviceIdentifier = AndroidIDDeviceIdentifier(context)
ANDROID_ID 在 Android 8.0(API 26)及更高版本上对每个用户账户和签名证书都是唯一的。在更早的 API 级别上它可能不可靠。采用此策略前请确认你的最低 API 要求。
自定义 Android 标识符
要在 Android 上提供一个完全自定义的标识符,实现 DeviceIdentifier 接口:
import com.pingidentity.deviceprofile.DeviceIdentifier
class CustomDeviceIdentifier(private val context: Context) : DeviceIdentifier {
override suspend fun id(): String {
// Return your custom stable identifier.
// SHA-256 hash of custom data is recommended for uniformity.
val customData = "org.example.${Build.SERIAL}"
return sha256(customData)
}
private fun sha256(input: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(input.toByteArray(Charsets.UTF_8))
return hash.joinToString("") { "%02x".format(it) }
}
}
在 React Native 桥接初始化之前,将实现注册到原生 SDK。
向 JavaScript 暴露原生标识符
如果你的应用需要在 JavaScript 中拿到设备标识符的值——例如展示它、在服务端记录它,或将其包含在自定义 API 调用中——请使用直接从 @ping-identity/rn-device-id 导出的 getDeviceId 函数:
import { getDeviceId } from '@ping-identity/rn-device-id';
export async function getDeviceId(): Promise<string> {
try {
const deviceId = await getDeviceId();
// Use deviceId to display, log server-side, or include in a custom API call
} catch (error) {
if (error instanceof DeviceIdError) {
console.error('Failed to retrieve device ID:', error.message);
}
}
}
// iOS: MyDeviceIdModule.swift
@objc(MyDeviceIdModule)
class MyDeviceIdModule: NSObject {
@objc
func getDeviceId(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
Task {
do {
let identifier = try DefaultDeviceIdentifier()
let id = try await identifier.id
resolve(id)
} catch {
reject("DEVICE_ID_ERROR", error.localizedDescription, error)
}
}
}
}
// Android: MyDeviceIdModule.kt
class MyDeviceIdModule(reactContext: ReactApplicationContext)
: ReactContextBaseJavaModule(reactContext) {
override fun getName() = "MyDeviceIdModule"
@ReactMethod
fun getDeviceId(promise: Promise) {
CoroutineScope(Dispatchers.IO).launch {
try {
val id = DefaultDeviceIdentifier.id()
promise.resolve(id)
} catch (e: Exception) {
promise.reject("DEVICE_ID_ERROR", e.message, e)
}
}
}
}Orchestration SDKs
Using device identifiers on React Native
PingOne Advanced Identity Cloud
PingAM
React Native
The Device Profiling module includes a device identifier in the collected profile. On both Android and iOS, the native SDK uses the same secure identifier generated by the Device ID module.
In React Native, you do not need to call @ping-identity/rn-device-id. The identifier is resolved automatically by the native layer when you call collectDeviceProfile. However, if you need to read the same identifier independently (for logging or display), you can use getDeviceId() from @ping-identity/rn-device-id directly.
Device identifiers serve two distinct roles in the SDK:
A unique identifier is embedded in the device profile payload sent to the server when a DeviceProfileCallback, PingOneProtectInitializeCallback, or PingOneProtectEvaluationCallback is processed.
A per-user key identifier (kid) is generated and stored alongside the cryptographic key pair when bindForJourney() registers a device.
Both roles are implemented in the underlying native platform SDKs. The React Native modules delegate identifier generation and storage to those native implementations. Understanding the platform defaults — and knowing when and how to customize them — helps you control identifier stability, sharing across apps, and recovery behavior.
How device identifiers work
The Orchestration SDK for React Native wraps native Android and iOS implementations. Device identifier generation happens entirely in native code; the JavaScript layer passes the identifier transparently as part of the profile or binding payload.
The table provides a platform comparison summary.
| Characteristic | iOS | Android |
|---|
Default identifier source
Keychain-backed RSA key pair (SHA-256 of public key)
Android KeyStore RSA key pair (SHA-256 of public key)
Persists across app reinstall
No — regenerated on reinstall
Persists across device restore
Yes (from encrypted backup)
Configurable from JavaScript
No — configure in native Xcode project
No — configure in native Android project
Alternate built-in strategy
AndroidIDDeviceIdentifier, LegacyDeviceIdentifier
Custom identifier support
Yes — implement DeviceIdentifier protocol
Yes — implement DeviceIdentifier interface
Yes — via Keychain Access Group
Yes — via AndroidIDDeviceIdentifier (API 26+)
iOS defaults
On iOS, the default device identifier is a SHA-256 hash of a public key generated and stored in the Keychain. The Keychain persists across app reinstalls (unless the device is wiped), giving the identifier strong stability.
Default identifier persistence behavior on iOS
| Scenario | Behavior |
|---|
App uninstall and reinstall
The identifier persists. The iOS Keychain is not cleared when an app is deleted, so the reinstalled app retrieves the same key.
This is not a standard user action on iOS. App reinstall is the closest equivalent, which preserves the identifier as noted above.
Device backup and restore
The identifier persists if restored from an encrypted iCloud or local backup, because these backups include Keychain data.
The identifier is permanently deleted. A factory reset clears all device storage, including the Keychain.
Sharing across apps (same developer)
The identifier can be shared across apps from the same developer using a Keychain Access Group. Configure the keychainAccount property in the native iOS SDK.
Android defaults
On Android, the default device identifier uses the Android KeyStore to generate and store an RSA key pair. The SHA-256 hash of the public key is the identifier.
Default identifier persistence behavior on Android
| Scenario | Behavior |
|---|
App uninstall and reinstall
The identifier is regenerated. Android KeyStore entries are scoped to the app’s UID, which changes on reinstall.
App data cleared (Settings > Clear Data)
The identifier is regenerated. Clearing app data removes the KeyStore entry.
Device backup and restore
The identifier is not restored. Android KeyStore entries are not included in standard backups.
The identifier is permanently deleted.
Not supported with the default KeyStore strategy. Use AndroidIDDeviceIdentifier (ANDROID_ID) if cross-app sharing is required on API 26+.
The persistence difference between iOS (persists across reinstalls) and Android (regenerated on reinstall) is important for server-side logic. If your Protect policy or device management rules depend on identifier stability, design for the less stable Android behavior to ensure cross-platform consistency.
Configuring device identifiers
Device identifier behavior in the React Native SDK is controlled by the underlying native platform modules. JavaScript configuration options are limited to concerns that are visible at the JS bridge boundary — primarily logging. Native-level customization (identifier strategy, Keychain account, KeyStore parameters) requires editing the native project files.
Attaching a logger for identifier diagnostics
Both collectDeviceProfile and createBindingClient accept a LoggerInstance.
Attaching a logger gives you visibility into identifier generation and retrieval operations:
import { collectDeviceProfile } from '@ping-identity/rn-device-profile';
import { createBindingClient } from '@ping-identity/rn-binding';
import { logger } from '@ping-identity/rn-logger';
const sdkLogger = logger({ level: 'debug' });
// Device profiling — logger shows native identifier resolution
const profile = await collectDeviceProfile(['platform', 'hardware', 'network'], {
logger: sdkLogger,
});
// Device binding — logger shows key generation and identifier storage
const bindingClient = createBindingClient({
logger: sdkLogger,
});
Customizing the default identifier on iOS
The iOS SDK’s DefaultDeviceIdentifier uses a Keychain-backed RSA key pair. To customize its configuration — such as sharing it across apps using a Keychain Access Group, changing the account name, or adjusting the key size — edit the native iOS module initialization in your Xcode project.
Customizing the Keychain account
By default the SDK stores the identifier under a Keychain account derived from your bundle ID.
To share the identifier across apps from the same developer, configure a shared Keychain Access Group:
// In your iOS AppDelegate or a native module initializer
import PingDeviceId
let config = DeviceIdentifierConfiguration(
keychainAccount: "com.example.shared.deviceid", // shared across your apps
useEncryption: true,
keySize: 2048
)
let deviceIdentifier = try DefaultDeviceIdentifier(configuration: config)
Pass this configured instance to the native SDK before the React Native bridge initializes the modules.
Do not disable encryption (useEncryption: false) in a production app.
Disabling encryption removes the security guarantees of the Keychain-backed identifier.
Regenerating the identifier
To force generation of a new identifier — for example, after a security incident or when a user explicitly requests device removal — call regenerateIdentifier() in native code:
// In a native module exposed to React Native
import PingDeviceId
func resetDeviceIdentifier() async throws -> String {
let deviceIdentifier = try DefaultDeviceIdentifier()
let newId = try await deviceIdentifier.regenerateIdentifier()
print("New Device ID: \(newId)")
return newId
}
Falling back to UUID-based identifiers
If Keychain operations fail on certain devices (for example, shared enterprise devices with restricted Keychain access), you can fall back to a simpler UUID-based identifier:
import PingDeviceId
// UUIDDeviceIdentifier stores a UUID in UserDefaults rather than the Keychain
let deviceIdentifier = try UUIDDeviceIdentifier()
let id = try await deviceIdentifier.id
UUIDDeviceIdentifier is less stable than the Keychain-backed default: the UUID is cleared when the app’s data is deleted. Use it only when Keychain access is not reliably available.
Creating a custom iOS identifier
For advanced use cases, implement the DeviceIdentifier protocol to supply a completely custom identifier:
import PingDeviceId
struct CustomDeviceIdentifier: DeviceIdentifier {
var id: String {
get async throws {
// Return your custom stable identifier.
// Ensure persistence — return the same value across app launches.
return try await loadOrGenerateStableId()
}
}
}
Register the custom implementation with the device profile or binding modules before the app starts processing journeys.
Customizing the default identifier on Android
The Android SDK provides three built-in identifier strategies and supports custom implementations.
Built-in Android identifier strategies
| Strategy | Persistence after reinstall | Description |
|---|
Uses Android KeyStore. Most secure. The default when no strategy is specified.
AndroidIDDeviceIdentifier
Uses Settings.Secure.ANDROID_ID. Persists across reinstalls on Android 8.0+ because the value is scoped to the signing certificate and user account. Resets on factory reset.
Backward-compatible strategy for apps migrating from older SDK versions. Use only when continuity with pre-migration identifiers is required.
Switching to AndroidIDDeviceIdentifier
To use ANDROID_ID as the device identifier — for example, to maintain a stable identifier across reinstalls — configure the native Android module:
// In your Android Application subclass or native module setup
import com.pingidentity.deviceprofile.DefaultDeviceIdentifier
import com.pingidentity.deviceprofile.AndroidIDDeviceIdentifier
// Replace the default strategy with ANDROID_ID
val deviceIdentifier = AndroidIDDeviceIdentifier(context)
ANDROID_ID is unique per user account and signing certificate on Android 8.0 (API 26) and later. On earlier API levels it may not be reliable. Verify your minimum API requirement before adopting this strategy.
Custom Android identifier
To supply a completely custom identifier on Android, implement the DeviceIdentifier interface:
import com.pingidentity.deviceprofile.DeviceIdentifier
class CustomDeviceIdentifier(private val context: Context) : DeviceIdentifier {
override suspend fun id(): String {
// Return your custom stable identifier.
// SHA-256 hash of custom data is recommended for uniformity.
val customData = "org.example.${Build.SERIAL}"
return sha256(customData)
}
private fun sha256(input: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(input.toByteArray(Charsets.UTF_8))
return hash.joinToString("") { "%02x".format(it) }
}
}
Register the implementation with the native SDK before the React Native bridge initializes.
Exposing native identifiers to JavaScript
If your application needs the device identifier value in JavaScript — for example, to display it, log it server-side, or include it in a custom API call — use the getDeviceId function exported directly from the @ping-identity/rn-device-id:
import { getDeviceId } from '@ping-identity/rn-device-id';
export async function getDeviceId(): Promise<string> {
try {
const deviceId = await getDeviceId();
// Use deviceId to display, log server-side, or include in a custom API call
} catch (error) {
if (error instanceof DeviceIdError) {
console.error('Failed to retrieve device ID:', error.message);
}
}
}
// iOS: MyDeviceIdModule.swift
@objc(MyDeviceIdModule)
class MyDeviceIdModule: NSObject {
@objc
func getDeviceId(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
Task {
do {
let identifier = try DefaultDeviceIdentifier()
let id = try await identifier.id
resolve(id)
} catch {
reject("DEVICE_ID_ERROR", error.localizedDescription, error)
}
}
}
}
// Android: MyDeviceIdModule.kt
class MyDeviceIdModule(reactContext: ReactApplicationContext)
: ReactContextBaseJavaModule(reactContext) {
override fun getName() = "MyDeviceIdModule"
@ReactMethod
fun getDeviceId(promise: Promise) {
CoroutineScope(Dispatchers.IO).launch {
try {
val id = DefaultDeviceIdentifier.id()
promise.resolve(id)
} catch (e: Exception) {
promise.reject("DEVICE_ID_ERROR", e.message, e)
}
}
}
}