详解安卓的FileProvider是如何提升文件共享安全的
zhezhongyun 2025-05-08 22:24 19 浏览
自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方法,那么我们不禁要问:PackageManager、ProviderInfo和XmlResourceParser这三个类在其中又起了什么作用了呢?要回答这个问题就需要先说一下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的逻辑也比较简单:
- 先创建一个SimplePathStrategy对象;
- 然后读取AndroidManifest中配置的file_paths.xml文件数据;
- 使用XmlResourceParser解析<paths>下的[路径标签]后,转换成对应路径的File;
- 通过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个解决思路,如果大家有更好的思路也欢迎在评论区留言:
- 将文件存储到应用的思路目录/data/data/<包名>目录下,这样除非在被Root的手机上,否则外部应用是无法直接读取的,但是一般受手机存储空间限制,在低配置的手机上无法存储比较大的文件。
- 实现自己的FileProvider,将file_paths.xml中的配置的“密钥”换一个使用对称加密处理并个地方存储,使用逆向难度比较大的NDK层加密生成加密后的Uri。
这些年Android也在从系统层面不断地提升自身的安全性,包括即将伴随着Android 11正式到来的分区存储(沙盒机制),能进一步地保证应用文件的安全性,后面我会单独的写一篇文章详细剖析安卓系统的分区存储(沙盒机制)。
针对FileProvider如何通过Uri提升安全性的问题今天就和大家讨论到这里,大家有任何问题欢迎在评论区留言。
相关推荐
- 用豆包生成的BMI计算器(豆包的热量是多少?)
-
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8...
- Android 开发中文引导-应用小部件
-
应用小部件是可以嵌入其它应用(例如主屏幕)并收到定期更新的微型应用视图。这些视图在用户界面中被叫做小部件,并可以用应用小部件提供者发布。可以容纳其他应用部件的应用组件叫做应用部件的宿主(1)。下面的截...
- Qt推流(视频文件/视频流/摄像头/桌面转流媒体rtmp+hls+webrtc)
-
一、前言说明推流直播就是把采集阶段封包好的内容传输到服务器的过程。其实就是将现场的视频信号从手机端,电脑端,摄影机端打包传到服务器的过程。“推流”对网络要求比较高,如果网络不稳定,直播效果就会很差,观...
- 一看就会!谷歌广告转化跟踪详细设置指南来了
-
在出海推广业务中,投放广告最常见的目的是获取订单,但我们怎么知道有没有达成投放目的呢?谷歌转化跟踪技术就可以做到!熟悉谷歌的卖家朋友都知道,转化跟踪在最近几年变得越来越复杂了,虽然有很多选项可以自定义...
- Android原生编解码接口MediaCodec详解
-
作者:躬行之MediaCodec是Android中的编解码器组件,用来访问底层提供的编解码器,通常与MediaExtractor、MediaSync、MediaMuxer、MediaCrypt...
- 手把手搭建RTSP流媒体服务器(rtsp 流媒体)
-
0.引言本文主要讲解如何搭建RTSP流媒体服务器的过程,使用开源项目ZLMediaKit。通过这个开源项目,推RTSP流到服务器,然后拉流端可以拉取RTSP、RTMP等流。ZLMediaKit码云链接...
- MediaInfo 24.04.0 是一个关于多媒体文件的信息提供工具
-
MediaInfo24.04.0是一个关于多媒体文件的信息提供工具(仅当文件中包含信息时才提供):包括常规信息(标题、作者、导演、专辑、曲目编号、日期、时长等);视频信息(编解码器、画面比例、帧率...
- rmvb格式视频怎么打开,rmvb转MP4认准这个方法
-
一、rmvb是什么格式? RMVB是一种视频文件格式,其中的VB指的是可变比特率。比起上一代的RM格式,RMVB 格式的画面比较清晰,因为它是降低了静态画面下的比特率。 二、制作rmvb ①...
- 教你用Plex Media Server,把铁威马变成你的“私人好莱坞”!
-
TNAS(铁威马NAS)中可以安装多媒体服务器、影视、PlexMediaServer、EmbyServer作为个人媒体服务器使用。PlexMediaServer可以组织整理TNAS上的媒体...
- 你肯定用过!经典Windows软件被抛弃
-
Windows系统这些年持续更新的过程中,不断融入新的软件和功能的同时,一些经典的应用也渐渐成为了历史……Windows媒体播放器被抛弃Windows系统不断地推陈出新,一些老旧的组件也难免被抛弃,在...
- 博思得Q8标签打印全能手(博思得标签打印机安装教程)
-
2014-12-0905:35:00作者:宋达希【中关村在线办公打印频道原创】服装吊牌、洗涤标签、产品说明标签等都要用到标签打印机,这些标签涵盖多种尺寸的长度和宽度以及材质。另外作为一件商品或者产...
- flv文件用什么播放器打开,这样做不踩雷!
-
FLV是FLASHVIDEO的简称,是随着FlashMX的推出发展而来的视频格式。它的出现有效地解决了视频文件导入Flash后,使导出的SWF文件体积庞大,不能在网络上很好的使用等问题。一、...
- media player怎么转换格式?音频转换神器推荐!
-
Windowsmediaplayer怎么转换格式?WindowsMediaPlayer是微软公司出品的一款多媒体播放器,通常简称“WMP”。提供了编辑音频和视频文件的功能。用户可以使用该软件导...
- 视频参数检查工具更新:MediaInfo 23.10
-
MediaInfo提供有关视频或音频文件的技术和标签信息。信息示例包括编解码器、比特率、每秒帧数、宽度、高度、频道数、持续时间、标题、作者、字幕语言和章节名称。多种方式可以查看信息(文本、工作表、树和...
- 多媒体管理软件:JRiver Media Center 31.0.68 (64位)
-
JRiverMediaCenter64位是适用于大量库的完整媒体解决方案。它组织、播放和标记所有类型的媒体文件,并对Xbox、PS3、UPnP、DLNA和TiVo进行翻录、刻录。JRiverM...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML常用标签 (29)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)
- CSS 水平对齐 (Horizontal Align) (30)