百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

详解安卓的FileProvider是如何提升文件共享安全的

zhezhongyun 2025-05-08 22:24 40 浏览

自Android 7.0开始,Android 框架开启了严格模式(StrictMode),禁止应用将file:///开头的Uri共享给其他的应用读写文件,否则会收到FileUriExposedException的异常。与此同时,Android框架提供了新的文件共享机制 — FileProvider

但在日常开发中大家使用FileProvider的机会比较少,故其背后的工作原理应该很少有人知道。在上一篇文章《Android系统为什么要提供FileProvider机制》中已经为大家讲解了FileProvider的作用是为了加强应用之间共享文件的安全性。仔细观察会发现,通过FileProvide生成的Uri是以content://开头,不同于以往file://开头的Uri直接暴露文件的存储的路径,FileProvide生成的Uri会使用我们在<paths>中配置的[路径标签]name属性替换真实的文件路径,有点类似掩码的机制,即使未经授权的外部App拿到了Uri也不知道文件的具体位置,更谈不上直接访问了,从而提高文件访问的安全性。

相信到了这里大家一定会好奇,FileProvider生成Uri的原理是什么?又是如何通过Uri提升文件安全的呢?带着这两个问题,我们一起来通过androidx版本中的FileProvider的源代码,一起探究一下FileProvider背后的机制和原理。

因为FileProvider比较不常用,相信有不少同学对如何配置FileProvider已经有点模糊了,为了方便大家理解下面的章节,我们先简单回顾一下FileProvide的配置方法。已经熟悉配置方法的同学可以跳过这一节直接看重点。

简单回顾如何配置FileProvider

提问:声明一个FileProvider一共分几步?:三步,第一步先把冰箱门打开,第二步把大象放进去....

不好意思,串台了,重来!

第一步:在Manifest文件中添加<provider>标签,设置android:name属性的值为
androidx.core.content.FileProvider;再设置
android:authorities属性的值,可以自定义,通常是应用的包名加上.fileprovider后缀;设置android:exported属性的值为false,表示拒绝外部直接访问;设置
android:grantUriPermissions
的属性为true,表示可以为文件赋予临时访问权限。示例如下:

<manifest>
    ...
    <application>
        ...
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.-"
            android:exported="false"
            android:grantUriPermissions="true">
            ...
        </provider>
        ...
    </application>
</manifest>

第二步:/res/xml文件夹下创建一个命名为file_paths.xml的路径配置文件(文件名可以自定义),在这个文件中创建<paths>根结点,并在该节点下配置共享的文件夹,示例配置如下:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/"/>
    ...
</paths>

<paths>可以包含一个或多个子节点,支持<root-path><files-path><cache-path><external-path><external-files-path><external-cache-path><external-media-path>七[路径标签],相同的标签可以出现多次来表示同一个父路径下的多个文件夹,在每种标签中,name属性是这个文件夹的别名,path属性是这个文件夹的真实路径名称,如前面所述,在生成Uri的时使用name别名来替换path中的真实路径,这样可以保护文件夹的真实路径不外泄。

下面来看一下<paths>标签支持的七种标签以及其对应的目录:

<paths>
    <!-- 对应的路径:File("/") -->
    <root-path name="root" path="my_root/" />
    <!-- 对应的路径:Context#getFilesDir() -->
    <files-path name="internal_files" path="my_internal_files/" />
    <!-- 对应的路径:Context#getCacheDir() -->
    <cache-path name="internal_cache" path="my_internal_cache/" />
    <!-- 对应的路径:Environment#getExternalStorageDirectory() -->
    <external-path name="external" path="my_external/" />
    <!-- 对应的路径:Context#getExternalFilesDirs(context, null) -->
    <external-files-path name="external_files" path="my_external_files/" />
    <!-- 对应的路径:Context#getExternalCacheDirs(context) -->
    <external-cache-path name="external_cache" path="my_external_cache/" />
    <!-- 对应的路径:Context#getExternalMediaDirs() -->
    <external-media-path name="external_media" path="my_external_media/" />
</paths>

另外还需要注意:在file_paths.xml文件中只能配置文件夹,不能配置单个文件;且一个[路径标签]中只能配置一个文件夹,不能配置多个文件夹。

最后一步:在第一步中定义的<provider>标签下使用<meta-data>标签引用这个配置,需要注意的是标签<meta-data>中的android:name属性必须是
android.support.FILE_PROVIDER_PATHS
。示例如下:

...
<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/file_paths"/>
</provider>
...

FileProvider生成Uri的过程

在第一节中我们回顾了在Manifest中声明FileProvider,本节咱们一起看一下FileProvider如何使用配置参数生成Uri。下面一起看一下如何使用FileProvider生成Uri:

File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
String authority = BuildConfig.APPLICATION_ID + ".fileprovider";
Uri contentUri = FileProvider.getUriForFile(context, authority, newFile);

通过以上代码生成Uri:
content://com.mydomain.fileprovider/my_images/default_image.jpg
,下面我们一起通过时序图来看一下关键方法
FileProvider.getUriForFile()
调用的背后到底发生了什么:

从以上时序图中我们可以很清晰地看到,最终负责生成Uri的是SimplePathStrategy类的getUriForFile方法,那么我们不禁要问:PackageManagerProviderInfoXmlResourceParser这三个类在其中又起了什么作用了呢?要回答这个问题就需要先说一下FileProvider生成Uri的三个步骤:

1)从缓存中查找PathStrategy:

我们先来看一下
FileProvider.getUriForFile()
的源代码:

public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
            @NonNull File file) {
    //调用getPathStrategy获取PathStrategy对象
    final PathStrategy strategy = getPathStrategy(context, authority);
    //调用PathStrategy生成Uri
    return strategy.getUriForFile(file);
}

从上面的源代码可以看到,
FileProvider.getUriForFile()
是一个门面方法,最终负责生成Uri的是PathStrategy对象,接下来我们看一下
FileProvider.getPathStrategy()
方法如何获取PathStrategy对象:

@GuardedBy("sCache")
private static HashMap<String, PathStrategy> sCache = new HashMap<String, PathStrategy>();
......
private static PathStrategy getPathStrategy(Context context, String authority) {
    PathStrategy strat;
    synchronized (sCache) {
        //以authority为key从缓存中读取PathStrategy
        strat = sCache.get(authority);
        if (strat == null) {
            try {
                //如果缓存中没有找到PathStrategy,调用parsePathStrategy方法
                //通过配置文件生成PathStrategy
                strat = parsePathStrategy(context, authority);
            } catch (IOException e) {
               ......
            }
            //将创建的PathStrategy方到缓存中
            sCache.put(authority, strat);
        }
    }
    return strat;
}

从上面的源代码可以看出,FileProvider使用HashMap实现了一个简单的缓存,通过传入的authority参数来存储不同的PathStrategy对象。进入getPathStrategy()方法,会先从缓存中查找PathStrategy,如果在缓存中没有,则会调用parsePathStrategy()方法创建一个,放到缓存中并使用。

2)读取Manifest配置创建PathStrategy:

我们再看一下parsePathStrategy()方法如何创建PathStrategy。

private static PathStrategy parsePathStrategy(Context context, String authority)
            throws IOException, XmlPullParserException {
    //使用传入的authority参数创建SimplePathStrategy对象
    final SimplePathStrategy strat = new SimplePathStrategy(authority);
	  //使用传入的authority参数读取AndroidManifest.xml配置的<provider>信息
    final ProviderInfo info = context.getPackageManager()
            .resolveContentProvider(authority, PackageManager.GET_META_DATA);
    ......
    //加载配置的file_paths.xml的数据
    final XmlResourceParser in = info.loadXmlMetaData(
            context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
    ......
    int type;
    while ((type = in.next()) != END_DOCUMENT) {
        if (type == START_TAG) {
          	//获取配置的[路径标签]:<root-path>、<files-path>、
            //<cache-path>、<external-path>、<external-files-path>、
            //<external-cache-path>或<external-media-path>
            final String tag = in.getName();
            //获取配置的name属性,即文件夹的别名
            final String name = in.getAttributeValue(null, ATTR_NAME);
            //获取配置的path属性,即文件夹的真实路径
            String path = in.getAttributeValue(null, ATTR_PATH);

            File target = null;
            //使用else if语句将[路径标签]转换成对应的真实路径File对象
            if (TAG_ROOT_PATH.equals(tag)) {
                target = DEVICE_ROOT;
            } else if (TAG_FILES_PATH.equals(tag)) {
            ......
            }
            //将文件夹的别名和真实路径添加到SimplePathStrategy对象中
            //buildPath()方法用[路径标签]的路径和path属性生成一个File对象
            if (target != null) {
                strat.addRoot(name, buildPath(target, path));
            }
        }
    }
    return strat;
}

如上面源代码注释,parsePathStrategy()方法创建PathStrategy的逻辑也比较简单:

  1. 先创建一个SimplePathStrategy对象;
  2. 然后读取AndroidManifest中配置的file_paths.xml文件数据;
  3. 使用XmlResourceParser解析<paths>下的[路径标签]后,转换成对应路径的File;
  4. 通过addRoot()方法将[路径标签]path路径添加到SimplePathStrategy对象中。

到了这里我们能看出来,SimplePathStrategy对象中维护了file_paths.xml中配置的各种路径,下面我们通过
SimplePathStrategy.addRoot()
源代码看一下SimplePathStrategy是如何维护各种路径的:

private final HashMap<String, File> mRoots = new HashMap<String, File>();
......
void addRoot(String name, File root) {
    if (TextUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Name must not be empty");
    }
    try {
        // 转换为规范路径名的文件
        // 例如:转换前文件路径 c:\users\..\program
        // Canonical转换的路径:C:\program
        root = root.getCanonicalFile();
    } catch (IOException e) {
        throw new IllegalArgumentException(
                "Failed to resolve canonical path for " + root, e);
    }
    mRoots.put(name, root);
}

从上面的代码可以看到SimplePathStrategy也是使用HashMap做了一个简单的缓存,使用文件夹的name别名来存储不同的文件夹规范路径后的File。

3)PathStrategy生成Uri:

经过前两个步骤的数据准备,终于到了最后一步:
SimplePathStrategy.getUriForFile():

@Override
public Uri getUriForFile(File file) {
    String path;
    try {
        //获取共享文件的规范路径字符串
        path = file.getCanonicalPath();
    } catch (IOException e) {
        throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
    }

    // 遍历缓存的文件夹,找出最具体根路径
    Map.Entry<String, File> mostSpecific = null;
    for (Map.Entry<String, File> root : mRoots.entrySet()) {
        final String rootPath = root.getValue().getPath();
        if (path.startsWith(rootPath) && (mostSpecific == null
                || rootPath.length() > mostSpecific.getValue().getPath().length())) {
            mostSpecific = root;
        }
    }
    //如果没有找到根路径则抛出异常
    if (mostSpecific == null) {
        throw new IllegalArgumentException(
                "Failed to find configured root that contains " + path);
    }

    // 去掉分享文件Path中的根路径
    final String rootPath = mostSpecific.getValue().getPath();
    if (rootPath.endsWith("/")) {
        path = path.substring(rootPath.length());
    } else {
        path = path.substring(rootPath.length() + 1);
    }

    // 使用authority、根路径别名和去掉根路径的Path生成最终的Uri
    path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
    return new Uri.Builder().scheme("content")
            .authority(mAuthority).encodedPath(path).build();
}

通过上面的源代码注释大家可以看到,Uri的生成的核心过程就是找到分享文件根路径的name别名的过程,然后Uri就能很轻松的构建出来了。

FileProvider通过Uri提升安全性

通过上面的源代码对FileProvider庖丁解牛,相信大家对FileProvider如何通过Uri提升安全性的问题都有了一定的认识。换个角度看,其实生成Uri的过程就是对文件路径进行加密的过程,使用的密钥就是我们在file_paths.xml文件中配置的路径name别名,这样及时外部应用拿到了文件的Uri也不知道文件具体的存储位置,所以就不能做到绕开授权直接文件了。

但是用逆向的思维来看也不是绝对安全的,如果我们想要破解一个应用生成的Uri对应的文件的绝对路径,只需要用apktool等逆向工具将file_paths.xml解压缩出来,根据file_paths.xml中的配置逆向解析Uri即可得到文件真正的路径了。

那么有没有更安全的方法来解决这个问题呢?在这里给大家提供2个解决思路,如果大家有更好的思路也欢迎在评论区留言:

  1. 将文件存储到应用的思路目录/data/data/<包名>目录下,这样除非在被Root的手机上,否则外部应用是无法直接读取的,但是一般受手机存储空间限制,在低配置的手机上无法存储比较大的文件。
  2. 实现自己的FileProvider,将file_paths.xml中的配置的“密钥”换一个使用对称加密处理并个地方存储,使用逆向难度比较大的NDK层加密生成加密后的Uri。

这些年Android也在从系统层面不断地提升自身的安全性,包括即将伴随着Android 11正式到来的分区存储(沙盒机制),能进一步地保证应用文件的安全性,后面我会单独的写一篇文章详细剖析安卓系统的分区存储(沙盒机制)。

针对FileProvider如何通过Uri提升安全性的问题今天就和大家讨论到这里,大家有任何问题欢迎在评论区留言。

相关推荐

Python入门学习记录之一:变量_python怎么用变量

写这个,主要是对自己学习python知识的一个总结,也是加深自己的印象。变量(英文:variable),也叫标识符。在python中,变量的命名规则有以下三点:>变量名只能包含字母、数字和下划线...

python变量命名规则——来自小白的总结

python是一个动态编译类编程语言,所以程序在运行前不需要如C语言的先行编译动作,因此也只有在程序运行过程中才能发现程序的问题。基于此,python的变量就有一定的命名规范。python作为当前热门...

Python入门学习教程:第 2 章 变量与数据类型

2.1什么是变量?在编程中,变量就像一个存放数据的容器,它可以存储各种信息,并且这些信息可以被读取和修改。想象一下,变量就如同我们生活中的盒子,你可以把东西放进去,也可以随时拿出来看看,甚至可以换成...

绘制学术论文中的“三线表”具体指导

在科研过程中,大家用到最多的可能就是“三线表”。“三线表”,一般主要由三条横线构成,当然在变量名栏里也可以拆分单元格,出现更多的线。更重要的是,“三线表”也是一种数据记录规范,以“三线表”形式记录的数...

Python基础语法知识--变量和数据类型

学习Python中的变量和数据类型至关重要,因为它们构成了Python编程的基石。以下是帮助您了解Python中的变量和数据类型的分步指南:1.变量:变量在Python中用于存储数据值。它们充...

一文搞懂 Python 中的所有标点符号

反引号`无任何作用。传说Python3中它被移除是因为和单引号字符'太相似。波浪号~(按位取反符号)~被称为取反或补码运算符。它放在我们想要取反的对象前面。如果放在一个整数n...

Python变量类型和运算符_python中变量的含义

别再被小名词坑哭了:Python新手常犯的那些隐蔽错误,我用同事的真实bug拆给你看我记得有一次和同事张姐一起追查一个看似随机崩溃的脚本,最后发现罪魁祸首竟然是她把变量命名成了list。说实话...

从零开始:深入剖析 Spring Boot3 中配置文件的加载顺序

在当今的互联网软件开发领域,SpringBoot无疑是最为热门和广泛应用的框架之一。它以其强大的功能、便捷的开发体验,极大地提升了开发效率,成为众多开发者构建Web应用程序的首选。而在Spr...

Python中下划线 ‘_’ 的用法,你知道几种

Python中下划线()是一个有特殊含义和用途的符号,它可以用来表示以下几种情况:1在解释器中,下划线(_)表示上一个表达式的值,可以用来进行快速计算或测试。例如:>>>2+...

解锁Shell编程:变量_shell $变量

引言:开启Shell编程大门Shell作为用户与Linux内核之间的桥梁,为我们提供了强大的命令行交互方式。它不仅能执行简单的文件操作、进程管理,还能通过编写脚本实现复杂的自动化任务。无论是...

一文学会Python的变量命名规则!_python的变量命名有哪些要求

目录1.变量的命名原则3.内置函数尽量不要做变量4.删除变量和垃圾回收机制5.结语1.变量的命名原则①由英文字母、_(下划线)、或中文开头②变量名称只能由英文字母、数字、下画线或中文字所组成。③英文字...

更可靠的Rust-语法篇-区分语句/表达式,略览if/loop/while/for

src/main.rs://函数定义fnadd(a:i32,b:i32)->i32{a+b//末尾表达式}fnmain(){leta:i3...

C++第五课:变量的命名规则_c++中变量的命名规则

变量的命名不是想怎么起就怎么起的,而是有一套固定的规则的。具体规则:1.名字要合法:变量名必须是由字母、数字或下划线组成。例如:a,a1,a_1。2.开头不能是数字。例如:可以a1,但不能起1a。3....

Rust编程-核心篇-不安全编程_rust安全性

Unsafe的必要性Rust的所有权系统和类型系统为我们提供了强大的安全保障,但在某些情况下,我们需要突破这些限制来:与C代码交互实现底层系统编程优化性能关键代码实现某些编译器无法验证的安全操作Rus...

探秘 Python 内存管理:背后的神奇机制

在编程的世界里,内存管理就如同幕后的精密操控者,确保程序的高效运行。Python作为一种广泛使用的编程语言,其内存管理机制既巧妙又复杂,为开发者们提供了便利的同时,也展现了强大的底层控制能力。一、P...