04、Manifest权限、安全控制与FileProvider
04、Manifest 权限、安全控制与 FileProvider
一、ContentProvider 为什么要特别关注安全
ContentProvider 的目标是“跨应用访问数据”。
既然能被其他 App 访问,就必须考虑:
- 哪些数据可以被访问?
- 哪些 App 可以访问?
- 只能读还是可以写?
- 是否只允许临时访问某个 URI?
- 是否会被 SQL 注入?
- 是否会误暴露私有文件?
ContentProvider 的安全控制不能只靠 UriMatcher,还需要 Manifest 权限和代码层校验配合。
二、provider 注册基础
最基本的注册方式:
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:exported="false" />
常见属性:
| 属性 | 说明 |
|---|---|
android:name |
Provider 类名 |
android:authorities |
Provider 唯一标识 |
android:exported |
是否允许其他 App 访问 |
android:permission |
读写共用权限 |
android:readPermission |
读权限 |
android:writePermission |
写权限 |
android:grantUriPermissions |
是否允许临时授予 URI 权限 |
android:process |
指定 Provider 所在进程 |
android:initOrder |
同一进程多个 Provider 初始化顺序 |
三、android:exported
android:exported 决定 Provider 是否可以被其他应用访问。
exported=false
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:exported="false" />
含义:
其他 App 默认不能直接访问这个 Provider
适合:
- Provider 只给自己 App 内部使用;
- Provider 只是初始化组件;
- 数据不希望对外暴露。
exported=true
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:exported="true" />
含义:
其他 App 可以访问这个 Provider
但实际是否能访问,还要看权限配置。
四、不要无脑 exported=true
错误做法:
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:exported="true" />
如果没有任何权限限制,其他 App 可能可以访问你暴露的数据。
更安全的做法:
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:exported="true"
android:readPermission="com.example.databasetest.permission.READ_PROVIDER"
android:writePermission="com.example.databasetest.permission.WRITE_PROVIDER" />
五、自定义权限
1. 定义权限
<permission
android:name="com.example.databasetest.permission.READ_PROVIDER"
android:protectionLevel="signature" />
<permission
android:name="com.example.databasetest.permission.WRITE_PROVIDER"
android:protectionLevel="signature" />
2. Provider 使用权限
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:exported="true"
android:readPermission="com.example.databasetest.permission.READ_PROVIDER"
android:writePermission="com.example.databasetest.permission.WRITE_PROVIDER" />
3. 调用方声明权限
<uses-permission android:name="com.example.databasetest.permission.READ_PROVIDER" />
六、permission、readPermission、writePermission 的区别
| 属性 | 作用 |
|---|---|
android:permission |
读写都需要这个权限 |
android:readPermission |
查询数据需要这个权限 |
android:writePermission |
插入、更新、删除需要这个权限 |
优先级上:
readPermission / writePermission 比 permission 更精细
如果读写权限不同,建议分开配置。
七、signature 权限适合自家 App 间共享
如果只是多个自家 App 之间共享数据,推荐使用:
android:protectionLevel="signature"
含义:
只有使用同一个签名证书打包的 App 才能获得权限
适合:
- 主 App 和插件 App;
- 用户端和商家端;
- 多个同公司应用;
- 系统定制 ROM 内置应用之间的数据共享。
八、grantUriPermissions 临时 URI 授权
有些场景不适合把整个 Provider 暴露出去,只想临时分享某个文件或某条数据。
这时可以使用临时 URI 授权。
Provider 侧:
<provider
android:name=".MyProvider"
android:authorities="com.example.app.provider"
android:exported="false"
android:grantUriPermissions="true" />
发送方:
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, "image/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
含义:
目标 App 原本没有权限访问 Provider
但这次 Intent 携带的 URI 可以临时读取
九、path-permission
如果不同路径需要不同权限,可以使用 <path-permission>。
示例:
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:exported="true">
<path-permission
android:pathPrefix="/public"
android:readPermission="com.example.permission.READ_PUBLIC" />
<path-permission
android:pathPrefix="/private"
android:readPermission="com.example.permission.READ_PRIVATE"
android:writePermission="com.example.permission.WRITE_PRIVATE" />
</provider>
适合:
同一个 Provider 下,不同 path 有不同安全级别
十、代码层也要做校验
Manifest 权限不是全部,Provider 内部也应该做必要校验。
例如:
private void checkBookWritePermission() {
Context context = getContext();
if (context == null) {
throw new SecurityException("Context is null");
}
int result = context.checkCallingOrSelfPermission(
"com.example.databasetest.permission.WRITE_PROVIDER"
);
if (result != PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("No write permission");
}
}
可以在 insert()、update()、delete() 中调用。
十一、防止 SQL 注入
危险写法:
String selection = "name = '" + userInput + "'";
如果 userInput 是:
' OR '1'='1
就可能造成查询条件被绕过。
安全写法:
String selection = "name = ?";
String[] selectionArgs = new String[]{userInput};
即:
db.query(
"Book",
projection,
"name = ?",
new String[]{userInput},
null,
null,
sortOrder
);
原则:
不要拼接用户输入到 SQL 条件中
使用 selectionArgs 传参数
十二、不要暴露敏感字段
即使 URI 只暴露了某张表,也不代表所有字段都应该返回。
例如用户表:
User(id, name, phone, token, password_hash, id_card)
对外查询时应该过滤字段:
private static final Map<String, String> USER_PROJECTION_MAP = new HashMap<>();
static {
USER_PROJECTION_MAP.put("_id", "_id");
USER_PROJECTION_MAP.put("name", "name");
USER_PROJECTION_MAP.put("phone", "phone");
}
不要返回:
- token;
- password hash;
- 身份证;
- 精确定位;
- 内部业务状态;
- 风控字段。
十三、FileProvider 是什么
FileProvider 是 AndroidX 提供的一个特殊 ContentProvider,主要用于安全分享文件。
它解决的问题是:
不要把 file:// 直接暴露给其他 App
改用 content:// URI,并通过临时授权访问
常见场景:
- 拍照后把图片分享给其他 App;
- 下载文件后调用系统安装器或查看器;
- 分享缓存目录中的文件;
- 打开 PDF、图片、音频、视频等文件。
十四、FileProvider Manifest 示例
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
注意:
| 属性 | 推荐值 |
|---|---|
android:exported |
false |
android:grantUriPermissions |
true |
authorities |
通常用 ${applicationId}.fileprovider |
十五、res/xml/filepaths.xml 示例
创建文件:
res/xml/filepaths.xml
内容:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path
name="files"
path="." />
<cache-path
name="cache"
path="." />
<external-files-path
name="external_files"
path="." />
</paths>
更安全的做法是只暴露必要子目录:
<cache-path
name="share_images"
path="images/" />
不要直接把整个外部存储大范围暴露出去。
十六、生成 FileProvider URI
File file = new File(getCacheDir(), "images/test.png");
Uri uri = FileProvider.getUriForFile(
this,
getPackageName() + ".fileprovider",
file
);
得到的 URI 类似:
content://com.example.app.fileprovider/cache/images/test.png
十七、分享文件给其他 App
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("image/png");
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intent, "分享图片"));
十八、安全清单
| 检查项 | 建议 |
|---|---|
| Provider 是否真的需要对外暴露 | 不需要就 exported=false |
| authority 是否唯一 | 使用包名风格 |
| 是否配置读写权限 | 对外暴露时必须考虑 |
| 是否区分读权限和写权限 | 写权限更敏感 |
| 是否使用 signature 权限 | 自家 App 共享优先考虑 |
| 是否使用临时 URI 授权 | 文件分享优先考虑 |
| 是否避免 SQL 拼接 | 使用 selectionArgs |
| 是否隐藏敏感字段 | 不要返回所有列 |
| 是否限制 FileProvider 路径 | 只暴露必要目录 |
| 是否处理异常 URI | NO_MATCH 要抛异常 |
| 是否记录敏感数据日志 | 不要在 Log 中打印隐私数据 |
十九、核心记忆
ContentProvider 安全控制可以分三层:
Manifest 层:
exported / permission / readPermission / writePermission / grantUriPermissions
URI 层:
authority / path / UriMatcher / path-permission
代码层:
参数化查询 / 字段过滤 / 调用方校验 / 敏感数据保护
不要只依赖一层安全机制。