IMEI 棄用!收下這份保姆級的 OAID 集成教程

你的支持對我意義重大!

?? Hi,我是旭銳。本文已收錄到 GitHub · Android-NoteBook 中。這里有 Android 進階成長路線筆記 & 博客,有志同道合的朋友,歡迎跟著我一起成長。(聯(lián)系方式 & 入群方式在 GitHub)

1. 前言

  • IMEI 等設備標識符已經(jīng)被認定為用戶隱私的一部分,在非必要的場景獲取甚至頻繁獲取 IMEI,會被認定為違規(guī)獲取用戶信息的行為;
  • 從 Android 10 開始,應用無法獲取 IMEI、MAC 等設備唯一標識(申請 READ_PRIVILEGED_PHONE_STATE 權限后也可以獲取,但這個權限只有系統(tǒng)應用能夠獲?。H绻麘脧娦蝎@取,則會獲取到 null 信息或發(fā)生 SecurityException(取決于 targetSdkVersion);
  • 為了解決這個問題,國家相關部委成立了移動安全聯(lián)盟 MSA,制定了一套《移動智能終端補充設備標識體系》。因為其中最重要的一個標識符是 OAID 匿名設備標識符,因此也有人把這個體系簡稱為 OAID。

2. 補充設備標識體系

補充設備標識體系主要分為四層結構:

  • UUID 設備唯一標識符 是不依賴于這個體系的,它們在設備出廠時就固化到硬件信息上了,即時恢復出廠設置也不會重置;
  • OAID 匿名設備標識 是 UUID 的替代品,在終端首次啟動時生成。同一設備的 OAID 相同,因此可以在多個應用之間共享,恢復出廠設置會重置 OAID;
  • VAID 開發(fā)者匿名設備標識符 是開發(fā)者維度的 ID,在應用安裝時生成。同一設備上且同一開發(fā)者的所有應用 VAID 相同,其他情況 VAID 不同,重新安裝應用會重置 VAID;
  • AAID 應用匿名設備標識符 是應用沙盤維度的 ID,在應用安裝時生成。即使是同一設備且同一個開發(fā)者的應用,AAID 也不同,重新安裝、清除用戶數(shù)據(jù)會重置 AAID。
英文縮寫 英文全稱 中文名稱 描述 重置性 應用場景
UDID Unique Device Identifier 設備唯一標識符 設備唯一硬件標識,由設備出廠時的硬件信息生成,如 IMEI 出廠后無法重置 設備唯一標識,如廣告業(yè)務在廣告投放時進行效果歸因
OAID Open Anonymous Device Identity 匿名設備標識符 OAID 是 UUID 的替代品,是設備維度的 ID。在終端首次啟動時生成,同一設備的 OAID 相同 恢復出廠設置后,OAID 會重置 設備唯一標識,如廣告業(yè)務在廣告投放時進行效果歸因
VAID Vender Anonymous Device Identity 開發(fā)者匿名設備標識符 VAID 是開發(fā)者維度的 ID。在應用安裝時生成,同一設備上且同一開發(fā)者的所有應用 VAID 相同,其他情況 VAID 不同 恢復出廠設置、重新安裝應用后,VAID 會重置(例外情況:卸載應用時如果設備中另有相同開發(fā)者的應用且讀取過 VAID,則不會重置) 開發(fā)者唯一標識,如果統(tǒng)一開發(fā)者不同瑩瑩之間的推薦
AAID Application Anonymous Device Identity 應用匿名設備標識符 AAID 是應用沙盤維度的 ID。在應用安裝時生成,即使是同一設備且同一個開發(fā)者的應用,AAID 也不同 恢復出廠設置、重新安裝、清除用戶數(shù)據(jù),AAID 會重置例外情況:卸載應用時如果設備中另有相同開發(fā)者的應用且讀取過 AAID,則不會重置) 應用唯一標識,可用于用戶統(tǒng)計

3. 準備工作

  • 注冊 MSA 賬號:

根據(jù) MSA 的要求,下載 SDK 和集成文檔前需要注冊一個企業(yè)賬戶,這一步按照指引提交相關信息和資料即可,一般 1~2 個工作日就可以審核通過。

  • 申請 SDK 證書:

從 v1.0.26 開始,SDK 引入了證書校驗機制,每個 APP 都需要申請一個證書文件(包名.cert.pem),并且只有包名與證書匹配的 APP 才能正常獲取補充設備 ID。默認證書的有效期為 1 年,證書過期也會影響獲取補充設備 ID。因此你還需要根據(jù)實際場景需要設計證書更新機制,比如在應用中內(nèi)置一個默認證書,并應用開到期時提前從后臺服務器更新證書。申請證書需要向 msa@caict.ac.cn 發(fā)送申請郵件,并附帶表格 example_batch.csv,例如:

  • 下載 SDK 與集成文檔:

企業(yè)賬號注冊并審核通過后,就可以從官網(wǎng)下載到相關資料了(因為 MSA 禁止第三方違規(guī)分發(fā) SDK,所以小伙伴們還是得自己去下載)。

  • 準備 vivo 商城 AppID:

    因為 vivo 手機的方案不同,在配置 SDK 時需要用到你的應用在應用商城上申請的 APPID。


4. 集成與封裝

  • 集成 aar: 依賴 oaid_sdk_x.x.x.aar,具體方法你肯定會了;
  • APPID 配置:supplierconfig.json 復制到項目 /main/assets 目錄下,并修改里面的 APPID 配置,主要是修改 vivo 的配置,例如:
  • 證書配置:包名.cert.pem 證書文件復制到項目 /main/assets 目錄下,例如:
  • 設置混淆: 配置 SDK 集成文檔中列舉的混淆配置,具體方法你肯定會了;

  • 設置 Gradle 編譯選項: 根據(jù)你的項目需要,配置 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 內(nèi)部的判斷也有關。

IOAIDApi.kt

interface IOAIDApi {

    /**
     * 1. 初始化,確保用戶同意《隱私政策》之后,再初始化 OAID SDK
     * 2. 加固版本在調用前必須載入SDK安全庫,因為加載有延遲,推薦在 Application 中調用 loadLibrary 方法
     *
     * @param debug 是否調試,debug 狀態(tài)會開啟 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:已經(jīng)初始化
     */
    @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: IOAID? = null

        @AnyThread
        fun oaid(context: Context): IOAID {
            if (null == _oaid) {
                synchronized(IOAID::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 統(tǒng)一的回調接收器
        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 內(nèi)部不會回調 onSupport
            InfoCode.INIT_ERROR_DEVICE_NOSUPPORT,       // 不支持的設備, SDK 內(nèi)部不會回調 onSupport
            InfoCode.INIT_ERROR_LOAD_CONFIGFILE,        // 加載配置文件出錯, SDK 內(nèi)部不會回調 onSupport
            InfoCode.INIT_ERROR_MANUFACTURER_NOSUPPORT, // 不支持的設備廠商, SDK 內(nèi)部不會回調 onSupport
            InfoCode.INIT_ERROR_SDK_CALL_ERROR          // SDK 調用出錯, SDK 內(nèi)部不會回調 onSupport
            -> {
                                // 異常情況,直接回調空信息
                listener.onSupport(unsupportedIdSupplier)
            }
            InfoCode.INIT_INFO_RESULT_DELAY,            // 獲取接口是異步的,SDK 內(nèi)部會回調 onSupport
            InfoCode.INIT_INFO_RESULT_OK                // 獲取接口是同步的,SDK 內(nèi)部會回調 onSupport
            -> {
                // do nothing
            }
            else -> {
                // do nothing
            }
        }
    }

    /**
     * 驗證與初始化證書
     * @return true:證書初始化成功;false:證書初始化失敗
     */
    private fun checkCertValidity() {

        /**
         * 從asset文件讀取證書內(nèi)容
         *
         * @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
            }
            isCertInit = true
            // 證書文件名
            val certStr = loadPemFromAssetFile()
            // 證書過期時間
            certExpDate = getCertExpDate(certStr)
            // TODO 如果你的應用場景對證書有效性要求非常高,可以在這個時機提前下載更新證書
            // 初始化證書,證書只需要初始化一次
            isCertValid = MdidSdkHelper.InitCert(context, certStr)
        }
    }
}

5. 其他細節(jié)

  • 隱私政策(重要): 因為 OAID 屬于第三方 SDK,所以你需要制定使用 SDK 獲取 ID 涉及的隱私政策,例如第三方 SDK 列表:
SDK 名稱 場景描述及特別說明 涉及收集的主要個人信息類型 數(shù)據(jù)接收方名稱 隱私政策鏈接
OAID 獲取 OAID,在廣告投放時進行效果歸因 設備信息(設備唯一標識) 中國信息通信研究院 http://www.msa-alliance.cn/
  • SDK 提供的接口:

    • IdSupplier#isSupported(): 判斷設備是否支持補充設備標識符
    • IdSupplier#isLimited(): 判斷設備是否限制應用獲取補充設備標識符
    • IdSupplier#getOAID(Context): 獲取 OAID
    • IdSupplier#getVAID(Context): 獲取 VAID
    • IdSupplier#getAAID(Context): 獲取 AAID
  • SDK 是否需要聯(lián)網(wǎng)?

多數(shù)廠商在調用接口時會要求聯(lián)網(wǎng),比如獲取 VAID、AAID 時需要去廠商后臺校驗和計算獲得

  • SDK 內(nèi)部如何判斷是否同一開發(fā)者的應用?

VAID 的定義是設備 + 開發(fā)者維度的 ID,同一設備上且同一開發(fā)者的所有應用 VAID 相同,其他情況 VAID 不同。不同手機廠商在判斷是否同一開發(fā)者的方式不同,有些是直接通過 AppId 判斷,如 vivo;有些是通過應用簽名信息判斷,如 oppo?;?AppID 的方案,要求我們在配置文件中填寫在應用商城中分配的 AppId(并配置到 assets/supplierconfig.json),基于應用簽名的方案,對我們會友好些。

  • SDK 是否支持模擬器?

補充設備標識符本質上是廠商提供的能力,因此只有真機支持,模擬器是不支持的。支持的真機列表見 SDK 文檔:

—— 圖片截圖自 MSA 官方文檔

  • 如何判斷證書的有效期?

可以通過代碼解析,也可以把證書內(nèi)容復制到 https://www.pianyissl.com/tools/cer_decoder 在線解析,例如:

不了解數(shù)字證書的小伙伴,可以回顧下我們之前的討論:《加密、摘要、簽名、證書,一次說明白!》

參考資料

最近,我建了一個 Android 交流群,希望大家可以一起討論技術,找到志同道合的朋友,需要的可以掃碼進群!

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容