【版权申明】非商业目的可自由转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/103125707
出自:shusheng007
相关文章
Android开发者之数据存储,你真的会存储数据吗?
前段时间Facebook的“隐私门”事件闹的沸沸扬扬,可见人们对于自己的数据安全性关注度越来越高。在可预见的将来,我们的生活会越来越数字化,数据安全问题将成为未来的头号问题。我们可以发现,这两年google对Android的升级主要是在安全性上做文章。Android已经过了那个野蛮生长的年代了,便利与安全问题的权衡将是未来重点。今天我们就看一下Android在App间共享文件的进化过程。
假设有两个App : AppProvider和AppConsumer 。 AppProvider要分享自己的女朋友照片 girlfriend.jpg
给AppConsumer ,就是下面这个小姐姐。
先看一下效果图:
上图演示了使用系统安装App安装一个新的app
上图演示了一个App读取其他App的分享文件的过程
在Android7.0之前,AppProvider 需要先把这张图片分享给AppConsumer 需要做如下几步:
这种方式存在什么问题呢?相信你已经猜到了,存在数据安全性问题!本来AppProvider 只是想给AppConsumer分享自己的女朋友照片,但是这样一来,其他App只要知道了这个文件地址都可以查看,万一是一张"门照片",AppProvider 就废了!以前网络不发达的时候没事,现在整不好就是一个门事件啊,还是小心为妙。
那怎么办呢,Android为此专门给出了一个解决方案,我们接着往下看。
Android 7.0 为此专门提供了一个叫 FileProvider的东西,它是ContentProvider的子类。我们可以使用它将文件路径映射为匿名Uri, 然后对此Uri授于临时访问权限。
当FileProvider被提出一段时间后我们就需要适配7.0了,虽然在搜索引擎的帮助下成功了,但是没有较深入的理解一下,直到第二次遇到相关问题的时候还是不太会,所以说对于一项技术只有理解了其原理才能轻松正确的使用。
我一贯认为,对于一个新的知识点,首先要可以正确的使用,然后再理解其原理,然后再回过头来看那些使用步骤就会有一种恍然大悟的感觉。那我们接下来先看一下如何通过FileProvider去安全的分享一个文件,其实只需如下简单的5步。
AndroidManifest.xml
中声明一个FileProvider
由于FileProvider
本质上是一个ContentProvider
,所以使用它的第一步自然是在AndroidManifest.xml
声明一下,如下代码所示
上面的代码使用了androidx
中的FileProvider
。值得注意的是其中的 android:exported
属性必须为false,android:grantUriPermissions
属性必须为true 。我们先看看如果设置exported为true会发生什么呢?你会发现运行时崩溃,报错日志如下:
java.lang.RuntimeException: Unable to get provider androidx.core.content.FileProvider: java.lang.SecurityException: Provider must not be exported
...
Caused by: java.lang.SecurityException: Provider must not be exported
at androidx.core.content.FileProvider.attachInfo(FileProvider.java:386)
...
日志非常清楚的告诉你 Provider must not be exported
,如果把grantUriPermissions设置为false效果一样,关于这个问题我们可以从源码中找到答案
在FileProvider
中有一个叫attachInfo()
的方法,这个方法的作用是将此provider
的信息提供给操作系统注册用的,其清晰的表明,这两个属性如果不满足要求就会抛SecurityException
异常。
/**
* After the FileProvider is instantiated, this method is called to provide the system with
* information about the provider.
*/
@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
super.attachInfo(context, info);
// Sanity check our security
if (info.exported) {
throw new SecurityException("Provider must not be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
mStrategy = getPathStrategy(context, info.authority);
}
FileProvider
要映射的文件路径文件我们会发现声明中包含了一个
标签。其name
属性为固定值,而resource
属性需要一个xm文件,这个文件就是用来做路径映射的。这个xml文件一般放在src/res/xml/
路径下,可任意命名。那它长什么样呢,分别代表什么意思呢,这块也是一个难点,反正我第一 次使用的时候没有搞太清楚。那让我们一起看一下它的一个例子
...
标签里面可以包括多个子标签,每个子标签对应Android系统中的一个文件路径,如果对这块不了解,请先阅读Android开发者之数据存储,你真的会存储数据吗?。
例如上面的文件中有两个子标签:
代表context.getExternalFilesDir(null)
获取到的文件路径,而
代表context.externalCacheDir
获取到的文件路径。
paths节点内部支持以下几个子节点,分别为:
代表Environment.getExternalStorageDirectory()
代表context.getFilesDir()
代表context.getCacheDir()
代表context.getExternalFilesDirs()
代表getExternalCacheDirs()
每个子标签里面又有两个属性,这两个属性分别代表什么意思呢?我们知道FileProvider
的原理就使将file:///
的Uri 替换为content://
的Uri,那么为了安全,我们肯定不希望产生出的Uri包含我们文件的具体路径信息吧,如果是那样的话,恶意用户就知道我们的文件的存储位置了。
我们看下面的映射关系:
file 路径:/storage/emulated/0/Android/data/top.ss007.devmemocompanion/files/myGoddess.jpg
uri 路径:content://top.ss007.devmemocompanion.fileProvider/external-files/myGoddess.jpg
通过对比可以发现具体的文件路径信息被替换了,那替换的规则是什么呢?秘密就隐藏在子标签的两个属性中:
name:我们得到的Uri格式为 content:// + 我们声明的那个FileProvider
的 authorities属性值 + name属性值+ 文件名称。例如我们在文件配置路径中name的值为external-files
。
path:这个属性值表示要映射的子路径,例如下面的子标签的意义为
表示可以映射的路径为 Context.externalCacheDir?.path + "/images"
及其子目录。什么意思呢?如果你有一个文件不在这个目录或者子目录下,对不起,映射会失败。所以有一种粗暴的做法就是将Android系统的所有目录都配置在这个文件下,那样就不会出错了,就像下面这样,本人不是太赞成这种方式。
值得注意的是,从这个xml文件支持的子标签可以看出,通过FileProvider
可以将存放在私有目录下的文件安全的分享给其他App。
当配置好了FileProvider
后就可以着手将文件映射为Uri了,调用 FileProvider
的如下方法即可
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority, @NonNull File file)
下面的代码对低版本做了兼容:
object FileUtils {
fun getUriForFile(context: Context, file:File): Uri {
if (Build.VERSION.SDK_INT>24){
return FileProvider.getUriForFile(context,context.packageName+".fileProvider",file)
}
return Uri.fromFile(file)
}
}
第二个参数为FileProvider
的android:authorities
属性值。
由于FileProvider
的android:exported
属性被声明为false ,所以必须对产生的Uri进行授权。推荐的授权方式为将此URI添加到Intent
的data中,然后设置权限flag给这个Intent
,如下代码所示。
这种授权方式的好处是,此授权是临时的,并且当接收App的任务栈(task stack)销毁时自动失效。代码如下所示
val intent=Intent().apply {
data = FileUtils.getUriForFile(this@MainActivity,file)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
note:其实除了上面的授权方式,Android还提供了另一套授权方式,但是不推荐使用。
使用如下代码对特定App及Uri授权
Context.grantUriPermission(String toPackage, Uri uri, int modeFlags)
使用如下代码撤销特定App及Uri的授权
Context.revokeUriPermission(String targetPackage, Uri uri, int modeFlags)
这种方式的缺点是,只要授权者不主动撤销接收App的权限,那么这个权限就一直有效。
有主动和被动两种方式提供Uri给接收App.
主动方式: startActivity(Intent intent)
例如我们要安装一个APK
到系统中,就是主动将Uri提供给系统安装App.如下面代码所示:
private fun installApk(act: Activity,file:File) {
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(
FileUtils.getUriForFile(act, file),
"application/vnd.android.package-archive"
)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(intent)
}
被动方式: startActivityForResult(Intent intent, int requestCode)
例如我们从相册App中选择一张照片
private fun selectImage(file: File) {
val intent = Intent().apply {
data = FileUtils.getUriForFile(this@MainActivity, file)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
setResult(Activity.RESULT_OK, intent)
finish()
}
只要获取到了文件Uri
,我们就可以通过ContentResolver
的ParcelFileDescriptor openFileDescriptor(@NonNull Uri uri, @NonNull String mode)
方法获得一个ParcelFileDescriptor
对象,然后通过其FileDescriptor getFileDescriptor()
方法获得FileDescrptor
. 只要获得FileDescrptor
就好说了,你可以转化为Stream保存成文件。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_GET_FILE) {
data?.data?.also { returnUri ->
val input = try {
contentResolver.openFileDescriptor(returnUri, "r")
} catch (e: FileNotFoundException) {
Log.e("MainActivity", "File not found.")
return
}
val fd = input?.fileDescriptor
ivImage.setImageBitmap(BitmapFactory.decodeFileDescriptor(fd))
}
}
}
FileProvider
的使用要点,首先其是一个ContentProvider
所以需要在AndroidMenifest.xml
文件中注册,其次需要隐藏真实文件路径,所以需要一个xml文档,最后就是授予接收App文件的临时访问权限。
通过上面的对比,FileProvider
的优势已经很明显了,安全便捷。对于发送者安全,对于使用者便捷。发送者可以将任意路径下的文件分享给其他App,例如存放在私有目录下的文件。对于使用者,读取文件不需要获取相应的存储权限。
看来想把自己的女朋友的照片安全的分享给别人也不容易啊!希望广大程序员增强数据安全意识,杜绝门事件。
源码gitbub地址: AndroidDevMemo