如何通过 Intent Filter 实现应用间的隐式启动?如何避免潜在的安全风险?
解读
在国内面试中,这道题考察的是“四大组件”通信基本功,同时把安全红线一起拉出来。面试官想确认三件事:
- 你是否真的写过
<intent-filter>
,知道它怎么被 PackageManagerService 解析、怎么参与匹配; - 你是否理解 Android 的“开放式”与“沙盒”并存的设计,知道隐式调用天生就是一次跨进程 RPC,必须做白名单校验;
- 你是否能把“功能正确”与“安全合规”同时落地,而不是写完能跑就交差。
答得太浅(“写个 action 就行”)会被追问细节;答得太深(“自己重写 IntentResolver”)会被质疑过度设计。因此要把“匹配流程 → 编码细节 → 国内合规 → 攻防案例”四段讲清,既体现深度又保持节奏。
知识点
- Intent 匹配三角:action、category、data(scheme/host/path/pathPattern/mimeType)。
- 解析者:PKMS 在扫描 APK 时把 filter 存进 IntentResolver 的二级索引(action → priority list),AMS 调用 IntentResolver.queryIntent 做匹配。
- 多候选返回列表时,系统按 priority 排序,同 priority 再弹“选择器”;国内 ROM 常在系统层做“链式唤醒”拦截,因此 priority>0 可能被强制降级。
- 安全模型:
(1) 导出标记:组件无 exported 属性且不带 intent-filter 时默认不导出;一旦写了 filter 默认 exported=true(Android 12 以后必须显式声明)。
(2) 签名级限制:自定义 signature permission 或 App-link 的 autoVerify,确保只有同签名或受信应用可调用。
(3) 数据级校验:对 uri 做 FileProvider、ContentProvider 的 grantUriPermissions,拒绝 file://。
(4) 国内合规:工信部 164 号文要求“显式用户同意 + 最小化场景”,链式唤醒、自启动、关联启动都要在系统设置里可关闭,否则应用商店审核驳回。 - 常见攻击面:
- Intent 重定向(Intent Redirection):把收到的 Intent 原封不动 startActivityForResult,导致特权提升。
- Scheme 劫持:恶意 App 注册相同 scheme 并设置更高 priority,抢在前端弹出钓鱼页。
- Parcel 侧信道:在 extra 里塞大对象,触发受害 App 在后台被拉活,增加电量统计。
- 防御 checklist:
(1) 最小化导出:仅暴露必要组件,其余显式 exported=false。
(2) 自定义权限:protectionLevel=signature|privileged,调用方必须申请。
(3) 对返回结果做包名校验:getCallingPackage() + getPackageManager().checkSignatures()。
(4) 对 extra 数据做大小、类型、深度校验,防止 Intent 重定向。
(5) 使用 App-link 代替一般 scheme,域名走工信部备案,通过 autoVerify=“true” 把劫持面降到 0。
(6) 国内上架前用华为/小米/OPPO 提供的“隐式调用扫描”工具跑一遍,确保无未保护导出现象。
答案
-
声明端(被启动方)
在 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。
-
调用端(启动方)
构造隐式 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。
-
安全风险规避
(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 则引导用户手动开启,否则应用商店审核会被认定为“强制自启动”。
拓展思考
- 双端签名权限虽然安全,但接入方需要同签名,业务拓展受限。可以考虑把权限拆成两层:
- 普通第三方:使用 custom permission + protectionLevel=dangerous,启动时弹自定义授权页,用户点击后写入 SharedPreference,作为“白名单缓存”。
- 受信合作方:继续走 signature 级别,代码里双通道判断,兼顾体验与安全。
- 如果业务必须暴露 uri,但文件又比较大,推荐用 FileProvider + 一次性授权 flag:
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
,并在目标组件的 onDestroy 里调用revokeUriPermission
及时回收,防止长期挂载。 - 国内厂商的“隐式调用日志”往往被裁剪,排查时可借助
adb shell am start -D -W
带 debug 参数,把 AMS 的 intent 匹配过程打到 logcat,过滤关键字 “IntentResolver” 即可看到 priority 排序与选择器弹窗原因。 - 未来 Android 14 开始,系统对“动态 intent 重定向”做了 SELinux 策略加固,即使通过反射拿到 IActivityManager 也绕不过。此时若业务确实需要把 Intent 转发给另一个组件,建议改用显式 Intent + Bundle 序列化,避免直接复用原对象,从而绕过新策略限制。