ContentProvider安全防护:守住数据的大门
说实话,ContentProvider 是 Android 四大组件里最容易被忽视的安全短板。很多人觉得它就是个数据库的包装壳,随便配配就能用。但我在项目中见过太多次因为 Provider 配置不当导致的数据泄露事故了。今天我就把 ContentProvider 的防护手段掰开揉碎了讲清楚。
核心原则:ContentProvider 默认是不安全的。你每开放一个 Provider,就等于在 App 的围墙上开了一扇门。我们要做的,就是决定这扇门开多大、谁来进、能拿什么。
一、exported=false:最基础的防线
先问个问题:你的 Provider 真的需要被其他 App 访问吗?
我见过不少开发者,写 Provider 时直接复制粘贴,exported=true 就这么留下了。其实大部分 Provider 只是给自己 App 内部用的。比如你 App 里有个数据库,只想让自己 App 的 Activity 去读,那就老老实实设成 false。
<provider
android:name=".MyProvider"
android:authorities="com.example.app.provider"
android:exported="false" />
嗯,就这么简单。但要注意,exported 的默认值其实有点坑——如果 Provider 里声明了 <intent-filter>,默认就是 true。所以我的习惯是:永远显式声明 exported 属性,别依赖默认值。
我曾经踩过的坑:有个同事写了个 Provider,加了 intent-filter 想支持隐式调用,结果忘了设 exported=false。上线后第三方 App 直接通过这个 Provider 把用户数据全拉走了。嗯,那周我们加班到凌晨三点。
二、权限保护:给门加上锁
如果 Provider 确实需要对外开放,那就得加权限。说白了,就是让其他 App 先出示「通行证」才能访问。
权限分两种:读权限和写权限。你可以分别控制。
<permission
android:name="com.example.permission.READ_DATA"
android:protectionLevel="dangerous" />
<provider
android:name=".MyProvider"
android:authorities="com.example.app.provider"
android:exported="true"
android:readPermission="com.example.permission.READ_DATA"
android:writePermission="com.example.permission.WRITE_DATA" />
这里有个细节:protectionLevel 选什么?我个人建议用 dangerous,因为 normal 级别的权限系统会自动授予,用户根本不知道。你想想看,用户都没同意,数据就被别的 App 读走了,这合理吗?
| 保护级别 | 特点 | 适用场景 |
|---|---|---|
| normal | 系统自动授予,无需用户确认 | 不敏感的数据,如设备信息 |
| dangerous | 需要用户弹窗确认 | 用户隐私数据,如联系人、位置 |
| signature | 仅同签名 App 可访问 | 自家 App 家族内部使用 |
小技巧:如果 Provider 只给自家 App 用,但又必须 exported=true(比如跨进程),用 signature 级别最省事。不用用户授权,也不用担心别人乱入。
三、参数化查询:防 SQL 注入的命门
这个我必须重点说。ContentProvider 底层往往是 SQLite,如果你直接拼接用户输入的字符串去查数据库,那跟裸奔没区别。
我见过最离谱的代码是这样的:
// ❌ 危险!千万别这么写
String selection = "name = '" + userInput + "'";
Cursor cursor = db.query("users", null, selection, null, null, null, null);
用户输入个 ' OR 1=1 --,你的数据库就全裸了。嗯,这招在 Web 上用了二十年,在 Android 上一样好使。
正确的做法是用参数化查询:
// ✅ 安全写法
String selection = "name = ?";
String[] selectionArgs = new String[]{userInput};
Cursor cursor = db.query("users", null, selection, selectionArgs, null, null, null);
为什么这样安全?因为 ? 占位符会把用户输入当成纯字符串处理,不会解析成 SQL 语句。说白了,就是数据库引擎帮你做了转义。
记住一条铁律:永远不要用字符串拼接的方式构造 SQL 语句。不管用户输入看起来多安全,都别信。我在项目中见过太多「我觉得用户不会这么输入」的案例,最后都翻车了。
四、临时 URI 权限:精准控制访问范围
有时候你只想让某个特定的 App 临时访问一下你的文件,比如用户从相册选了一张图发给微信。这时候给全局权限太过了,临时 URI 权限就派上用场了。
实现方式很简单:
<provider
android:name=".FileProvider"
android:authorities="com.example.app.fileprovider"
android:exported="true"
android:grantUriPermissions="true" />
然后在代码里通过 Intent 授予临时权限:
Intent intent = new Intent(Intent.ACTION_SEND);
Uri uri = FileProvider.getUriForFile(context, "com.example.app.fileprovider", file);
intent.setData(uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
这样,只有你指定的那个 Intent 接收方才能访问这个 URI。一旦 Intent 处理完,权限就自动回收了。干净利落。
我的习惯:只要涉及文件分享,一律用 FileProvider + 临时 URI 权限。别用 file:// 协议,Android 7.0 以后已经禁了。而且临时权限比全局权限安全得多,用完即焚,不留后患。
知识体系总览
下面这张图把 ContentProvider 的防护手段串起来了,你可以对照着看:
这四层防护不是互斥的,你可以组合使用。比如一个 Provider 既设了 exported=true 加权限保护,又在 query 方法里用了参数化查询。说白了,安全没有银弹,多一层防护就多一分安心。
最后提醒一句:ContentProvider 的安全不是配完就完事的。每次版本迭代,都要重新审视 Provider 的配置。我见过太多 App 因为加了个新功能,顺手把 Provider 的 exported 改成 true,结果忘了加权限,数据就漏了。嗯,安全是持续的过程,不是一锤子买卖。