随着全球隐私法规趋严(GDPR/CCPA/PIPL)和Google Play政策升级,设备标识符处理不当将导致应用下架或高额罚款。本文将提供一套完整的合规解决方案,包含代码级实现细节。
一、设备标识符合规核心原则
原则 | 技术要求 | 违规风险 |
---|---|---|
最小化收集 | 仅获取业务必需的标识符 | 收集冗余数据罚款 |
用户知情权 | 动态弹窗+隐私政策双重告知 | 监管机构处罚 |
用户控制权 | 提供一键撤销机制 | 应用商店下架 |
安全存储 | 硬件级加密存储 | 数据泄露法律诉讼 |
有限留存期 | 自动过期删除机制 | 侵犯用户被遗忘权 |
二、合规标识符使用指南(附代码对比)
1. 允许使用的标识符
AAID(广告ID)最佳实践:
// 获取AAID的合规方式
suspend fun getAAID(context: Context): String? {
return try {
val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context)
// 关键检查:用户是否启用广告追踪限制
if (adInfo.isLimitAdTrackingEnabled) {
null // 用户禁用追踪时返回空
} else {
adInfo.id // 返回有效的AAID
}
} catch (e: Exception) {
Log.e("AdId", "获取AAID失败", e)
null
}
}
// 使用示例(在协程中调用)
lifecycleScope.launch {
val aaid = getAAID(requireContext())
aaid?.let { storeAAID(it) } // 存储前必须获得用户同意
}
Android ID(SSAID)注意事项:
// 获取Android ID的合规方式
fun getAndroidId(context: Context): String {
return Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
).takeIf { !it.isNullOrEmpty() } ?: generateScopedId(context)
}
// 生成作用域ID(Android 10+替代方案)
private fun generateScopedId(context: Context): String {
val prefs = context.getSharedPreferences("device_id", MODE_PRIVATE)
return prefs.getString("scoped_id", null) ?: run {
val newId = UUID.randomUUID().toString()
prefs.edit().putString("scoped_id", newId).apply()
newId
}
}
2. 禁止使用的标识符(附替代方案)
禁止标识符 | 违规风险 | 替代方案 |
---|---|---|
IMEI/MEID | Google Play立即下架 | 使用AAID + 设备特征哈希 |
MAC地址 | Android 6.0+无法获取 | 随机生成UUID(作用域存储) |
序列号 | 违反Google Play政策 | Firebase Instance ID |
// 设备特征哈希生成示例(替代硬件ID)
fun getDeviceFingerprint(context: Context): String {
val deviceInfo = """
${Build.MANUFACTURER}|${Build.MODEL}|${Build.BRAND}
${context.resources.configuration.screenLayout}
${Locale.getDefault().country}
""".trimIndent()
return try {
val digest = MessageDigest.getInstance("SHA-256")
digest.digest(deviceInfo.toByteArray()).joinToString("") {
"%02x".format(it)
}
} catch (e: Exception) {
UUID.randomUUID().toString()
}
}
三、合规存储方案实现(四层防护)
1. 存储架构设计
2. 本地加密存储(Android Keystore)
object SecureStorage {
private const val KEY_ALIAS = "com.example.encryption_key"
private const val PREFS_NAME = "secure_prefs"
// 初始化AES加密
private fun getCipher(mode: Int): Cipher {
val key = getOrCreateKey()
return Cipher.getInstance("AES/GCM/NoPadding").apply {
init(mode, key)
}
}
private fun getOrCreateKey(): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
if (!keyStore.containsAlias(KEY_ALIAS)) {
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").apply {
init(
KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(false)
.setKeySize(256)
.build()
)
generateKey()
}
}
return (keyStore.getEntry(KEY_ALIAS, null) as KeyStore.SecretKeyEntry).secretKey
}
// 加密数据
fun encrypt(context: Context, data: String): String {
val cipher = getCipher(Cipher.ENCRYPT_MODE)
val iv = cipher.iv
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
// 存储IV和加密数据
context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit {
putString("iv", Base64.encodeToString(iv, Base64.NO_WRAP))
putString("data", Base64.encodeToString(encrypted, Base64.NO_WRAP))
}
return Base64.encodeToString(encrypted, Base64.NO_WRAP)
}
// 解密数据
fun decrypt(context: Context): String? {
val prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
val ivString = prefs.getString("iv", null) ?: return null
val dataString = prefs.getString("data", null) ?: return null
return try {
val cipher = getCipher(Cipher.DECRYPT_MODE)
val iv = Base64.decode(ivString, Base64.NO_WRAP)
val encrypted = Base64.decode(dataString, Base64.NO_WRAP)
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), GCMParameterSpec(128, iv))
String(cipher.doFinal(encrypted), Charsets.UTF_8)
} catch (e: Exception) {
Log.e("SecureStorage", "解密失败", e)
null
}
}
}
3. 服务端安全存储
// Spring Boot示例 - 数据库字段加密
@Converter
public class CryptoConverter implements AttributeConverter<String, String> {
@Value("${encryption.key}")
private String encryptionKey;
public String convertToDatabaseColumn(String attribute) {
// 使用KMS或HSM管理的密钥
return AES.encrypt(attribute, encryptionKey);
}
public String convertToEntityAttribute(String dbData) {
return AES.decrypt(dbData, encryptionKey);
}
}
// 实体类注解
@Entity
public class DeviceInfo {
@Id
private Long id;
@Convert(converter = CryptoConverter.class)
private String deviceId; // 自动加密存储
@Column(name = "is_sensitive")
private boolean sensitive = true; // 标记敏感字段
}
四、用户同意框架实现(完整流程)
1. 合规流程
2. 动态同意弹窗实现
class ConsentDialog : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.dialog_consent, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<Button>(R.id.btn_accept).setOnClickListener {
// 用户同意后处理
(activity as? MainActivity)?.onConsentGranted()
dismiss()
}
view.findViewById<Button>(R.id.btn_reject).setOnClickListener {
// 用户拒绝处理
(activity as? MainActivity)?.onConsentDenied()
dismiss()
}
// 隐私政策链接可点击
view.findViewById<TextView>(R.id.privacy_link).movementMethod = LinkMovementMethod.getInstance()
}
}
// 在主Activity中调用
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (!Prefs.getConsentStatus()) {
ConsentDialog().show(supportFragmentManager, "consent_dialog")
}
}
fun onConsentGranted() {
Prefs.saveConsentStatus(true)
lifecycleScope.launch {
// 获取并存储AAID
getAAID(this@MainActivity)?.let { id ->
SecureStorage.encrypt(this@MainActivity, id)
}
}
}
fun onConsentDenied() {
Prefs.saveConsentStatus(false)
// 使用匿名模式
Analytics.setUserProperty("tracking_enabled", "false")
}
}
五、自动删除机制实现
1. WorkManager定时删除
class DataExpirationWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// 删除过期数据
deleteExpiredIds()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
private suspend fun deleteExpiredIds() {
withContext(Dispatchers.IO) {
val prefs = applicationContext.getSharedPreferences("device_data", MODE_PRIVATE)
val lastUpdate = prefs.getLong("last_updated", 0)
// 30天过期策略
if (System.currentTimeMillis() - lastUpdate > 30 * 24 * 3600 * 1000L) {
prefs.edit().clear().apply()
}
}
}
}
// 在应用启动时调度
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
scheduleDataExpiration()
}
private fun scheduleDataExpiration() {
val request = PeriodicWorkRequestBuilder<DataExpirationWorker>(
1, TimeUnit.DAYS // 每天检查一次
).setInitialDelay(1, TimeUnit.DAYS)
.build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"data_expiration",
ExistingPeriodicWorkPolicy.KEEP,
request
)
}
}
2. 用户触发删除(GDPR要求)
fun handleUserDataDeletion(userId: String) {
// 本地数据删除
SecureStorage.clear(applicationContext)
// 请求服务端删除
lifecycleScope.launch {
try {
retrofitService.deleteUserData(userId)
} catch (e: Exception) {
// 重试逻辑
}
}
}
六、第三方SDK合规集成
1. 广告SDK配置示例
// 初始化时禁用自动收集
MobileAds.initialize(context) {
// 关键配置:禁用SDK自动收集设备ID
val config = RequestConfiguration.Builder()
.setTagForChildDirectedTreatment(
RequestConfiguration.TAG_FOR_CHILD_DIRECTED_TREATMENT_TRUE
)
.setMaxAdContentRating(RequestConfiguration.MAX_AD_CONTENT_RATING_G)
.build()
MobileAds.setRequestConfiguration(config)
}
// 传递用户广告偏好
val adRequest = AdRequest.Builder()
.apply {
if (Prefs.isAdPersonalized()) {
// 用户同意个性化广告
addKeyword("technology")
} else {
// 用户选择非个性化
setHttpTimeoutMillis(5000)
}
}
.build()
2. 签署数据处理协议(DPA)
// 检查清单
- [ ] 确认SDK支持`setRestrictedDataProcessing(true)`
- [ ] SDK隐私政策链接加入应用隐私声明
- [ ] 独立获取用户同意后再初始化SDK
- [ ] 定期审计SDK网络请求(使用Charles Proxy)
七、合规自检清单
-
代码扫描:
# 检查危险API调用 grep -r "getDeviceId()" src/ grep -r "Build.SERIAL" src/
-
网络流量检查:
-
Google Play预检:
关键总结
-
标识符选择原则:
-
安全存储四要素:
- 客户端:Android Keystree硬件加密
- 传输层:HTTPS + 证书绑定
- 服务端:字段级加密 + KMS
- 存储层:敏感数据标记
-
生命周期管理:
// 伪代码总结 object DeviceIdManager { fun init() { /* 检查用户同意 */ } fun getId() { /* 返回合规ID */ } fun reset() { /* 用户触发删除 */ } fun autoClean() { /* 定时任务 */ } }
-
违规成本矩阵:
违规行为 风险概率 损失预估 收集IMEI 100% $200万起罚 + 下架 未提供撤回选项 80% 全球收入4%罚款 加密措施不足 60% 用户集体诉讼
通过本方案,开发者可构建符合全球隐私法规的标识符管理系统,平衡业务需求与合规要求。建议每季度进行合规审计,及时跟进政策变化。