IMEI 弃用!收下这份保姆级的 OAID 集成教程

简介: IMEI 弃用!收下这份保姆级的 OAID 集成教程

1. 前言


  • IMEI 等设备标识符已经被认定为用户隐私的一部分,在非必要的场景获取甚至频繁获取 IMEI,会被认定为违规获取用户信息的行为;
  • 从 Android 10 开始,应用无法获取 IMEI、MAC 等设备唯一标识(申请 READ_PRIVILEGED_PHONE_STATE 权限后也可以获取,但这个权限只有系统应用能够获取)。如果应用强行获取,则会获取到 null 信息或发生 SecurityException(取决于 targetSdkVersion);
  • 为了解决这个问题,国家相关部委成立了移动安全联盟 MSA,制定了一套《移动智能终端补充设备标识体系》。因为其中最重要的一个标识符是 OAID 匿名设备标识符,因此也有人把这个体系简称为 OAID。


2. 补充设备标识体系


补充设备标识体系主要分为四层结构:


  • UUID 设备唯一标识符 是不依赖于这个体系的,它们在设备出厂时就固化到硬件信息上了,即使恢复出厂设置也不会重置;
  • OAID 匿名设备标识 是 UUID 的替代品,在终端首次启动时生成。同一设备的 OAID 相同,因此可以在多个应用之间共享,恢复出厂设置会重置 OAID;
  • VAID 开发者匿名设备标识符 是开发者维度的 ID,在应用安装时生成。同一设备上且同一开发者的所有应用 VAID 相同,其他情况 VAID 不同,重新安装应用会重置 VAID;
  • AAID 应用匿名设备标识符 是应用沙盘维度的 ID,在应用安装时生成。即使是同一设备且同一个开发者的应用,AAID 也不同,重新安装、清除用户数据会重置 AAID。


英文缩写 英文全称 中文名称 描述 重置性 应用场景
UDID Unique Device Identifier 设备唯一标识符 设备唯一硬件标识,由设备出厂时的硬件信息生成,如 IMEI 出厂后无法重置 设备唯一标识,如广告业务在广告投放时进行效果归因
OAID Open Anonymous Device Identity 匿名设备标识符 OAID 是 UUID 的替代品,是设备维度的 ID。在终端首次启动时生成,同一设备的 OAID 相同 恢复出厂设置后,OAID 会重置 设备唯一标识,如广告业务在广告投放时进行效果归因
VAID Vender Anonymous Device Identity 开发者匿名设备标识符 VAID 是开发者维度的 ID。在应用安装时生成,同一设备上且同一开发者的所有应用 VAID 相同,其他情况 VAID 不同 恢复出厂设置、重新安装应用后,VAID 会重置(例外情况:卸载应用时如果设备中另有相同开发者的应用且读取过 VAID,则不会重置) 开发者唯一标识,如果统一开发者不同莹莹之间的推荐
AAID Application Anonymous Device Identity 应用匿名设备标识符 AAID 是应用沙盘维度的 ID。在应用安装时生成,即使是同一设备且同一个开发者的应用,AAID 也不同 恢复出厂设置、重新安装、清除用户数据,AAID 会重置例外情况:卸载应用时如果设备中另有相同开发者的应用且读取过 AAID,则不会重置) 应用唯一标识,可用于用户统计



3. 准备工作


  • 注册 MSA 账号:


根据 MSA 的要求,下载 SDK 和集成文档前需要注册一个企业账户,这一步按照指引提交相关信息和资料即可,一般 1~2 个工作日就可以审核通过。

image.png

  • 申请 SDK 证书:

从 v1.0.26 开始,SDK 引入了证书校验机制,每个 APP 都需要申请一个证书文件(包名.cert.pem),并且只有包名与证书匹配的 APP 才能正常获取补充设备 ID。默认证书的有效期为 1 年,证书过期也会影响获取补充设备 ID。因此你还需要根据实际场景需要设计证书更新机制,比如在应用中内置一个默认证书,并应用开到期时提前从后台服务器更新证书。申请证书需要向 msa@caict.ac.cn 发送申请邮件,并附带表格 example_batch.csv,例如:

image.png


  • 下载 SDK 与集成文档:

企业账号注册并审核通过后,就可以从官网下载到相关资料了(因为 MSA 禁止第三方违规分发 SDK,所以小伙伴们还是得自己去下载)。

image.png

  • 准备 vivo 商城 AppID:
    因为 vivo 手机的方案不同,在配置 SDK 时需要用到你的应用在应用商城上申请的 APPID。


4. 集成与封装

  • 集成 aar: 依赖 oaid_sdk_x.x.x.aar,具体方法你肯定会了;
  • APPID 配置:supplierconfig.json 复制到项目 /main/assets 目录下,并修改里面的 APPID 配置,主要是修改 vivo 的配置,例如:

image.png

  • 证书配置:包名.cert.pem 证书文件复制到项目 /main/assets 目录下,例如:

image.png

  • 设置混淆: 配置 SDK 集成文档中列举的混淆配置,具体方法你肯定会了;
  • 设置 Gradle 编译选项: 根据你的项目需要,配置 ndk 编译配置,例如:


// ndk {
//    abiFilters 'armeabi-v7a', 'x86', 'arm64-v8a', 'x86_64'
// }
splits {
    abi {
        enable true
        reset()
        include 'armeabi-v7a', 'arm64-v8a'
        universalApk true
    }
}
packagingOptions {
    doNotStrip "*/armeabi-v7a/*.so"
    doNotStrip "*/arm64-v8a/*.so"
}
复制代码
  • 代码封装: 完成以上集成和配置步骤后,剩下就是调用 SDK 接口获取 OAID 了。官方提供的 Demo 中 DemoHelper 代码质量一般,我简单整理了一版,代码不难,你直接看吧。有一些点要注意下:
  • 1、隐私政策授权: 需要确保用户同意《隐私政策》后,再初始化 SDK,懂得都懂;
  • 2、初始化时机: 加固版本在调用前需要先 loadLibrary(”msaoaidsec”),因为加载有延迟,所以官方推荐尽早提前初始化;
  • 3、回调时机:MidSdkHelper#InitSdk() 的结果回调可能是同步的,也可能是从异步子线程回调,与 SDK 内部的判断也有关。

IOAIDApi.kt


interface IOAIDApi {
    /**
     * 1. 初始化,确保用户同意《隐私政策》之后,再初始化 OAID SDK
     * 2. 加固版本在调用前必须载入SDK安全库,因为加载有延迟,推荐在 Application 中调用 loadLibrary 方法
     *
     * @param debug 是否调试,debug 状态会开启 SDK 日志输出
     */
    fun init(debug: Boolean)
    /**
     * 获取 ID,回调可能是同步的,也可能是异步的
     */
    @AnyThread
    fun fetchDeviceIds(callback: (OAIDResult) -> Unit)
}
复制代码

OAID.kt


internal class OAID private constructor(
    context: Context
) : IOAIDApi {
    // ApplicationContext
    private val context: Context = context.applicationContext
    /**
     * 证书初始化标记,true:已经初始化
     */
    @Volatile
    private var isCertInit: Boolean = false
    /**
     * 证书是否有效,true:有效
     */
    private var isCertValid: Boolean = false
    /**
     * 证书过期时间,null:证书无效
     */
    private var certExpDate: Date? = null
    /**
     * 调试开关
     */
    private var debug: Boolean = false
    companion object {
        @Volatile
        private var _oaid: IOAIDApi? = null
        @AnyThread
        fun oaid(context: Context): IOAIDApi {
            if (null == _oaid) {
                synchronized(IOAIDApi::class.java) {
                    if (null == _oaid) {
                        _oaid = OAIDImpl(context)
                    }
                }
            }
            return _oaid!!
        }
    }
    override fun init(debug: Boolean) {
        System.loadLibrary("msaoaidsec")
        this.debug = debug
    }
    override fun fetchDeviceIds(callback: (OAIDResult) -> Unit) {
        // 1. 验证与初始化证书
        checkCertValidity()
        // 2.1 提供空信息的 ID 提供器
        val unsupportedIdSupplier = IdSupplierImpl()
        // 2.2 统一的回调接收器
        val listener = IIdentifierListener { supplier: IdSupplier? ->
            supplier?.let {
                // 回调
                callback(
                    OAIDResult(
                        supplier.isSupported, supplier.isLimited, supplier.oaid, supplier.vaid, supplier.aaid
                    )
                )
            }
        }
        if (!isCertValid) {
            // 证书无效,直接回调空信息
            listener.onSupport(unsupportedIdSupplier)
            return
        }
        // 3. 调用 SDK 接口获取 OAID
        val code = try {
            MdidSdkHelper.InitSdk(context, debug, listener)
        } catch (error: Error) {
            error.printStackTrace()
            -1
        }
        // 4. 处理异常情况
        when (code) {
            InfoCode.INIT_ERROR_CERT_ERROR,             // 证书未初始化或证书无效,SDK 内部不会回调 onSupport
            InfoCode.INIT_ERROR_DEVICE_NOSUPPORT,       // 不支持的设备, SDK 内部不会回调 onSupport
            InfoCode.INIT_ERROR_LOAD_CONFIGFILE,        // 加载配置文件出错, SDK 内部不会回调 onSupport
            InfoCode.INIT_ERROR_MANUFACTURER_NOSUPPORT, // 不支持的设备厂商, SDK 内部不会回调 onSupport
            InfoCode.INIT_ERROR_SDK_CALL_ERROR          // SDK 调用出错, SDK 内部不会回调 onSupport
            -> {
                // 异常情况,直接回调空信息
                listener.onSupport(unsupportedIdSupplier)
            }
            InfoCode.INIT_INFO_RESULT_DELAY,            // 获取接口是异步的,SDK 内部会回调 onSupport
            InfoCode.INIT_INFO_RESULT_OK                // 获取接口是同步的,SDK 内部会回调 onSupport
            -> {
                // do nothing
            }
            else -> {
                // do nothing
            }
        }
    }
    /**
     * 验证与初始化证书
     * @return true:证书初始化成功;false:证书初始化失败
     */
    private fun checkCertValidity() {
        /**
         * 从asset文件读取证书内容
         *
         * @return 证书字符串
         */
        fun loadPemFromAssetFile(): String {
            return try {
                // 证书文件名
                val certFileName = context.packageName + ".cert.pem"
                val inputStream = context.assets.open(certFileName)
                val bufferReader = BufferedReader(InputStreamReader(inputStream))
                val builder = StringBuilder()
                var line: String?
                while (bufferReader.readLine().also { line = it } != null) {
                    builder.append(line)
                    builder.append('\n')
                }
                builder.toString()
            } catch (e: IOException) {
                ""
            }
        }
        /**
         * 解析证书过期时间
         * @return 过期时间,证书不合法时返回 null
         */
        fun getCertExpDate(certStr: String): Date? {
            return try {
                // 证书实体
                val cert = CertificateFactory.getInstance("X.509").generateCertificate(certStr.byteInputStream()) as X509Certificate
                // 验证证书有效性,如果证书过期会抛出异常
                cert.checkValidity()
                cert.notAfter
            } catch (ex: Exception) {
                // 证书无效
                null
            }
        }
        // DCL
        if (isCertInit) {
            // 初始化只需要进行一次,返回上一次的结果
            return
        }
        synchronized(IOAID::class) {
            if (isCertInit) {
                return
            }
            // 证书文件名
            val certStr = loadPemFromAssetFile()
            // 证书过期时间
            certExpDate = getCertExpDate(certStr)
            // TODO 如果你的应用场景对证书有效性要求非常高,可以在这个时机提前下载更新证书
            // 初始化证书,证书只需要初始化一次
            isCertValid = MdidSdkHelper.InitCert(context, certStr)
            isCertInit = true
        }
    }
}
复制代码



5. 其他细节


  • 隐私政策(重要): 因为 OAID 属于第三方 SDK,所以你需要制定使用 SDK 获取 ID 涉及的隐私政策,例如第三方 SDK 列表:


SDK 名称 场景描述及特别说明 涉及收集的主要个人信息类型 数据接收方名称 隐私政策链接
OAID 获取 OAID,在广告投放时进行效果归因 设备信息(设备唯一标识) 中国信息通信研究院 www.msa-alliance.cn/


  • SDK 提供的接口:
  • IdSupplier#isSupported(): 判断设备是否支持补充设备标识符
  • IdSupplier#isLimited(): 判断设备是否限制应用获取补充设备标识符
  • IdSupplier#getOAID(Context): 获取 OAID
  • IdSupplier#getVAID(Context): 获取 VAID
  • IdSupplier#getAAID(Context): 获取 AAID
  • SDK 是否需要联网?


多数厂商在调用接口时会要求联网,比如获取 VAID、AAID 时需要去厂商后台校验和计算获得


  • SDK 内部如何判断是否同一开发者的应用?


VAID 的定义是设备 + 开发者维度的 ID,同一设备上且同一开发者的所有应用 VAID 相同,其他情况 VAID 不同。不同手机厂商在判断是否同一开发者的方式不同,有些是直接通过 AppId 判断,如 vivo;有些是通过应用签名信息判断,如 oppo。基于 AppID 的方案,要求我们在配置文件中填写在应用商城中分配的 AppId(并配置到 assets/supplierconfig.json),基于应用签名的方案,对我们会友好些。


  • SDK 是否支持模拟器?

补充设备标识符本质上是厂商提供的能力,因此只有真机支持,模拟器是不支持的。支持的真机列表见 SDK 文档:

image.png


—— 图片截图自 MSA 官方文档


  • 如何判断证书的有效期?


可以通过代码解析,也可以把证书内容复制到 www.pianyissl.com/tools/cer_d… 在线解析,例如:

image.png

不了解数字证书的小伙伴,可以回顾下我们之前的讨论:《加密、摘要、签名、证书,一次说明白!》

目录
相关文章
|
2月前
|
机器学习/深度学习 Python
CatBoost高级教程:深度集成与迁移学习
CatBoost高级教程:深度集成与迁移学习【2月更文挑战第17天】
29 1
|
2月前
|
机器学习/深度学习 算法 Python
CatBoost中级教程:集成学习与模型融合
CatBoost中级教程:集成学习与模型融合【2月更文挑战第13天】
48 3
|
3月前
|
机器学习/深度学习 算法 Python
LightGBM高级教程:深度集成与迁移学习
LightGBM高级教程:深度集成与迁移学习【2月更文挑战第6天】
114 4
|
8月前
|
存储 网络协议 物联网
Android集成MQTT教程:实现高效通信和实时消息传输
Android集成MQTT教程:实现高效通信和实时消息传输
771 0
|
2月前
|
jenkins Java 持续交付
Docker搭建持续集成平台Jenkins最简教程
Jenkins 是一个广泛使用的开源持续集成工具,它能够自动化构建、测试和部署软件项目。在本文中,我们将使用 Docker 搭建一个基于 Jenkins 的持续集成平台。
141 2
|
6月前
|
消息中间件 数据可视化 Java
消息中间件系列教程(22) -Kafka- SpringBoot集成Kafka
消息中间件系列教程(22) -Kafka- SpringBoot集成Kafka
72 0
|
7月前
|
Web App开发 搜索推荐 NoSQL
如何搭建一个集成导航与在线工具的个性化浏览器私有书签(附详细搭建教程)
在这个信息爆炸的时代,我们都希望拥有一个能够轻松解决多端、多浏览器的收藏和笔记同步问题的神奇工具。Mtab书签正是为此而设计的顶级应用。它将基础导航、记事本、在线小工具和多端同步集于一身,为用户提供了更便利的网络浏览体验,并解决了多端同步的烦恼。
174 0
如何搭建一个集成导航与在线工具的个性化浏览器私有书签(附详细搭建教程)
|
3月前
|
Java Maven
【SpringBoot专题_02】springboot集成Swagger详细教程
【SpringBoot专题_02】springboot集成Swagger详细教程
|
3月前
|
Java easyexcel Maven
【Java专题_04】集成EasyExcel进行Excel导入导出详细教程
【Java专题_04】集成EasyExcel进行Excel导入导出详细教程
|
4月前
|
机器学习/深度学习 前端开发 Python
Scikit-Learn 中级教程——集成学习
Scikit-Learn 中级教程——集成学习
51 0