如何通过 Intent Filter 实现应用间的隐式启动?如何避免潜在的安全风险?

解读

在国内面试中,这道题考察的是“四大组件”通信基本功,同时把安全红线一起拉出来。面试官想确认三件事:

  1. 你是否真的写过 <intent-filter>,知道它怎么被 PackageManagerService 解析、怎么参与匹配;
  2. 你是否理解 Android 的“开放式”与“沙盒”并存的设计,知道隐式调用天生就是一次跨进程 RPC,必须做白名单校验;
  3. 你是否能把“功能正确”与“安全合规”同时落地,而不是写完能跑就交差。
    答得太浅(“写个 action 就行”)会被追问细节;答得太深(“自己重写 IntentResolver”)会被质疑过度设计。因此要把“匹配流程 → 编码细节 → 国内合规 → 攻防案例”四段讲清,既体现深度又保持节奏。

知识点

  1. Intent 匹配三角:action、category、data(scheme/host/path/pathPattern/mimeType)。
  2. 解析者:PKMS 在扫描 APK 时把 filter 存进 IntentResolver 的二级索引(action → priority list),AMS 调用 IntentResolver.queryIntent 做匹配。
  3. 多候选返回列表时,系统按 priority 排序,同 priority 再弹“选择器”;国内 ROM 常在系统层做“链式唤醒”拦截,因此 priority>0 可能被强制降级。
  4. 安全模型:
    (1) 导出标记:组件无 exported 属性且不带 intent-filter 时默认不导出;一旦写了 filter 默认 exported=true(Android 12 以后必须显式声明)。
    (2) 签名级限制:自定义 signature permission 或 App-link 的 autoVerify,确保只有同签名或受信应用可调用。
    (3) 数据级校验:对 uri 做 FileProvider、ContentProvider 的 grantUriPermissions,拒绝 file://。
    (4) 国内合规:工信部 164 号文要求“显式用户同意 + 最小化场景”,链式唤醒、自启动、关联启动都要在系统设置里可关闭,否则应用商店审核驳回。
  5. 常见攻击面:
    • Intent 重定向(Intent Redirection):把收到的 Intent 原封不动 startActivityForResult,导致特权提升。
    • Scheme 劫持:恶意 App 注册相同 scheme 并设置更高 priority,抢在前端弹出钓鱼页。
    • Parcel 侧信道:在 extra 里塞大对象,触发受害 App 在后台被拉活,增加电量统计。
  6. 防御 checklist:
    (1) 最小化导出:仅暴露必要组件,其余显式 exported=false。
    (2) 自定义权限:protectionLevel=signature|privileged,调用方必须申请。
    (3) 对返回结果做包名校验:getCallingPackage() + getPackageManager().checkSignatures()。
    (4) 对 extra 数据做大小、类型、深度校验,防止 Intent 重定向。
    (5) 使用 App-link 代替一般 scheme,域名走工信部备案,通过 autoVerify=“true” 把劫持面降到 0。
    (6) 国内上架前用华为/小米/OPPO 提供的“隐式调用扫描”工具跑一遍,确保无未保护导出现象。

答案

  1. 声明端(被启动方)
    在 AndroidManifest.xml 中给组件增加 <intent-filter>,至少声明一个 <action>;如需精准匹配,再补充 <category android:name="android.intent.category.DEFAULT"/><data> 子节点。
    示例:让 OtherApp 打开本应用内的 PdfViewActivity

    <activity
        android:name=".PdfViewActivity"
        android:exported="true"
        android:permission="com.myapp.permission.OPEN_PDF"
        android:label="查看PDF">
        <intent-filter>
            <action android:name="com.myapp.action.VIEW_PDF" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="application/pdf" />
        </intent-filter>
    </activity>
    

    同时在 manifest 里定义权限:

    <permission
        android:name="com.myapp.permission.OPEN_PDF"
        android:protectionLevel="signature" />
    

    这样只有与 com.myapp 同签名的调用方才能通过隐式 Intent 启动 PdfViewActivity,系统会在 AMS 层做 UID 签名对比,失败直接抛 SecurityException。

  2. 调用端(启动方)
    构造隐式 Intent,必须完全命中 filter 中的 action、category、data:

    val intent = Intent("com.myapp.action.VIEW_PDF").apply {
        addCategory(Intent.CATEGORY_DEFAULT)
        setDataAndType(uri, "application/pdf")
    }
    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
    } else {
        Toast.makeText(context, "未安装支持的应用", Toast.LENGTH_SHORT).show()
    }
    

    注意:

    • 国内 ROM 对后台启动 Activity 有限制,建议先用 startActivityForResult 或加上 FLAG_ACTIVITY_NEW_TASK,并在前台服务或可见窗口里调用。
    • 若 targetSdk>=31,还需在 manifest 里声明 <queries><package android:name="com.myapp" /></queries>,否则 resolveActivity 返回 null。
  3. 安全风险规避
    (1) 默认导出问题:Android 12 起,任何包含 intent-filter 的组件必须显式写 exported,否则编译失败;低于 12 的设备也建议统一显式声明,防止被意外导出。
    (2) 拒绝 Intent 重定向:在 PdfViewActivity 的 onCreate 里,先通过 getCallingPackage() 和 checkSignatures() 确认来源,再把收到的 Intent 封装成新 Intent 重新校验,禁止直接把原 Intent 传给 setResult。
    (3) 防止 scheme 劫持:对外提供 H5 跳转时,使用 https 域名 + App-link,并在服务器端放置 assetlinks.json,确保系统把隐式调用直接分发给本应用,不弹选择器;同时把 filter 的 autoVerify 设为 true。
    (4) 国内合规:在隐私政策里声明“关联启动”场景,并在代码里调用系统接口 startActivityAsUser 前,先判断 Settings.canShowBackgroundStartActivity(),如返回 false 则引导用户手动开启,否则应用商店审核会被认定为“强制自启动”。

拓展思考

  1. 双端签名权限虽然安全,但接入方需要同签名,业务拓展受限。可以考虑把权限拆成两层:
    • 普通第三方:使用 custom permission + protectionLevel=dangerous,启动时弹自定义授权页,用户点击后写入 SharedPreference,作为“白名单缓存”。
    • 受信合作方:继续走 signature 级别,代码里双通道判断,兼顾体验与安全。
  2. 如果业务必须暴露 uri,但文件又比较大,推荐用 FileProvider + 一次性授权 flag:
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION),并在目标组件的 onDestroy 里调用 revokeUriPermission 及时回收,防止长期挂载。
  3. 国内厂商的“隐式调用日志”往往被裁剪,排查时可借助 adb shell am start -D -W 带 debug 参数,把 AMS 的 intent 匹配过程打到 logcat,过滤关键字 “IntentResolver” 即可看到 priority 排序与选择器弹窗原因。
  4. 未来 Android 14 开始,系统对“动态 intent 重定向”做了 SELinux 策略加固,即使通过反射拿到 IActivityManager 也绕不过。此时若业务确实需要把 Intent 转发给另一个组件,建议改用显式 Intent + Bundle 序列化,避免直接复用原对象,从而绕过新策略限制。