11、跨进程数据共享:Android 权限模型、Provider 权限声明、签名权限
跨进程数据共享,说白了就是让你的 App 能访问另一个 App 的数据。嗯,这听起来简单,但实际做起来坑不少。我早期做企业级应用时,就因为在权限配置上少写了一个属性,导致合作方的 App 死活读不到我们的数据。排查了一下午,最后发现是 exported 没设成 true。这种低级错误,犯过一次就再也不会忘了。
今天我们来聊聊 Android 的权限模型,以及如何给 ContentProvider 声明正确的权限。你想想看,数据都暴露给外部了,不设防怎么行?
Android 权限模型:三层防护
Android 的权限体系,我习惯把它理解成「三层门禁」:
- 第一层:安装时授权 —— 用户在安装 App 时,系统会列出所有
normal级别的权限,用户同意后才能继续。比如访问网络、读取手机状态等。 - 第二层:运行时授权 —— 从 Android 6.0 开始,
dangerous级别的权限需要在运行时动态申请。比如读取联系人、访问相机等。用户可以在设置里随时关闭。 - 第三层:签名权限 —— 这是最高级别的防护。只有使用相同签名的 App 才能访问。说白了,就是「自己人」才能进。
对于 ContentProvider 来说,我们主要关注的是第二层和第三层。因为 Provider 暴露的是结构化数据,一旦被恶意应用读取,后果很严重。我在项目中遇到过,某个金融类 App 因为 Provider 权限没配好,导致用户的交易记录被第三方应用抓取。嗯,这属于严重的安全事故了。
Provider 权限声明:两种方式
给 ContentProvider 加权限,有两种主流方式。我建议你根据业务场景来选择:
方式一:在 <provider> 标签中直接声明
这是最直观的方式。你可以在 AndroidManifest.xml 里这样写:
<provider
android:name=".MyProvider"
android:authorities="com.example.myapp.provider"
android:exported="true"
android:readPermission="com.example.permission.READ_DATA"
android:writePermission="com.example.permission.WRITE_DATA" />
然后,你需要在同一个 Manifest 文件中定义这两个权限:
<permission
android:name="com.example.permission.READ_DATA"
android:protectionLevel="dangerous" />
<permission
android:name="com.example.permission.WRITE_DATA"
android:protectionLevel="dangerous" />
这样,任何想要读取你 Provider 数据的 App,都必须先声明 <uses-permission android:name="com.example.permission.READ_DATA" />,并且在运行时动态申请。如果用户拒绝,查询就会返回空结果或者抛出异常。
android:exported 设为 true。否则外部应用根本连不上你的 Provider,权限声明也就失去了意义。
方式二:使用 android:permission 统一声明
如果你不想区分读和写,可以用一个统一的权限来控制所有操作:
<provider
android:name=".MyProvider"
android:authorities="com.example.myapp.provider"
android:exported="true"
android:permission="com.example.permission.ACCESS_DATA" />
这种方式适合那些「要么全给,要么全不给」的场景。我个人习惯在内部工具类 App 中使用这种方式,因为管理起来更简单。
签名权限:最高级别的信任
签名权限,说白了就是「认章不认人」。只要两个 App 的签名证书相同,它们就能互相访问数据。这种方式非常适合同一个开发团队或同一个公司的多个 App 之间共享数据。
声明签名权限很简单:
<permission
android:name="com.example.permission.SHARED_DATA"
android:protectionLevel="signature" />
然后在 Provider 中引用:
<provider
android:name=".SharedProvider"
android:authorities="com.example.shared.provider"
android:exported="true"
android:permission="com.example.permission.SHARED_DATA" />
这样,只有和你使用相同签名的 App 才能访问这个 Provider。其他 App 就算在 Manifest 里声明了 com.example.permission.SHARED_DATA,也会被系统拒绝。
权限检查的完整流程
当外部 App 尝试通过 ContentResolver 访问你的 Provider 时,系统会做以下检查:
- 检查
exported属性 —— 如果为false,直接拒绝。 - 检查是否有
permission属性 —— 如果有,检查调用方是否拥有该权限。 - 检查
readPermission/writePermission—— 根据操作类型(读/写)分别检查。 - 检查签名权限 —— 如果权限的
protectionLevel是signature,系统会比对两个 App 的签名。
任何一个环节不通过,系统就会抛出 SecurityException。嗯,这里要注意,你的 Provider 代码里最好捕获这个异常,避免 App 崩溃。
知识体系图
下面这张图总结了本章的核心逻辑,你可以对照着理解:
避坑指南
最后,分享几个我踩过的坑:
- 我曾经忘记在 Manifest 中声明自定义权限 —— 结果 Provider 的
readPermission引用了不存在的权限,系统直接忽略了权限检查,导致数据完全公开。嗯,这个 bug 藏得很深,直到安全审计才发现。 - 签名权限的误用 —— 如果你把
protectionLevel设成了signatureOrSystem,那么系统级 App 也能访问你的数据。这通常不是你想要的。我建议除非有特殊需求,否则只用signature。 - 运行时权限的兼容性 —— 如果你的 App 还要支持 Android 5.1 及以下版本,记得在代码里做版本判断。低版本系统不会自动请求运行时权限,你需要引导用户去设置里手动开启。
exported 设为 true 却不加任何权限。这等于把你的数据库直接暴露给全世界。我在代码审查时见过不少这样的案例,每次都会打回去要求修改。
好了,关于跨进程数据共享的权限模型,我们就聊到这里。记住一句话:权限不是限制,而是保护。保护你的数据,也保护用户的隐私。
公众号:蓝海资料掘金营,微信deep3321