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

代码层:
  参数化查询 / 字段过滤 / 调用方校验 / 敏感数据保护

不要只依赖一层安全机制。