Android开发之如何在App间安全地共享文件(FileProvider详解)?

【版权申明】非商业目的可自由转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/103125707
出自:shusheng007

相关文章
Android开发者之数据存储,你真的会存储数据吗?

文章目录

  • 概述
  • 共享文件
    • Android7.0之前
    • Android7.0之后
      • 如何实施
        • 第一步: 在`AndroidManifest.xml`中声明一个`FileProvider`
        • 第二步: 配置此`FileProvider`要映射的文件路径文件
        • 第三步:将文件路径映射为Uri
        • 第四步:对获得的Uri进行授权
        • 第五步:将此Uri 提供给使用者
      • 如何读取文件
  • 总结

概述

前段时间Facebook的“隐私门”事件闹的沸沸扬扬,可见人们对于自己的数据安全性关注度越来越高。在可预见的将来,我们的生活会越来越数字化,数据安全问题将成为未来的头号问题。我们可以发现,这两年google对Android的升级主要是在安全性上做文章。Android已经过了那个野蛮生长的年代了,便利与安全问题的权衡将是未来重点。今天我们就看一下Android在App间共享文件的进化过程。

共享文件

假设有两个App : AppProviderAppConsumer 。 AppProvider要分享自己的女朋友照片 girlfriend.jpg 给AppConsumer ,就是下面这个小姐姐。

Android开发之如何在App间安全地共享文件(FileProvider详解)?_第1张图片
先看一下效果图:
Android开发之如何在App间安全地共享文件(FileProvider详解)?_第2张图片
上图演示了使用系统安装App安装一个新的app
Android开发之如何在App间安全地共享文件(FileProvider详解)?_第3张图片
上图演示了一个App读取其他App的分享文件的过程

Android7.0之前

在Android7.0之前,AppProvider 需要先把这张图片分享给AppConsumer 需要做如下几步:

  1. 将图片放到文件系统的非私有目录下
  2. 将图片访问权限设置为可读
  3. 将图片的地址告诉AppConsumer
  4. AppConsumer还必须拥有外部存储读取权限

这种方式存在什么问题呢?相信你已经猜到了,存在数据安全性问题!本来AppProvider 只是想给AppConsumer分享自己的女朋友照片,但是这样一来,其他App只要知道了这个文件地址都可以查看,万一是一张"门照片",AppProvider 就废了!以前网络不发达的时候没事,现在整不好就是一个门事件啊,还是小心为妙。

那怎么办呢,Android为此专门给出了一个解决方案,我们接着往下看。

Android7.0之后

Android 7.0 为此专门提供了一个叫 FileProvider的东西,它是ContentProvider的子类。我们可以使用它将文件路径映射为匿名Uri, 然后对此Uri授于临时访问权限

当FileProvider被提出一段时间后我们就需要适配7.0了,虽然在搜索引擎的帮助下成功了,但是没有较深入的理解一下,直到第二次遇到相关问题的时候还是不太会,所以说对于一项技术只有理解了其原理才能轻松正确的使用。

我一贯认为,对于一个新的知识点,首先要可以正确的使用,然后再理解其原理,然后再回过头来看那些使用步骤就会有一种恍然大悟的感觉。那我们接下来先看一下如何通过FileProvider去安全的分享一个文件,其实只需如下简单的5步。

如何实施

第一步: 在AndroidManifest.xml中声明一个FileProvider

由于FileProvider本质上是一个ContentProvider,所以使用它的第一步自然是在AndroidManifest.xml声明一下,如下代码所示

	
	    
            
        
		

上面的代码使用了androidx中的FileProvider。值得注意的是其中的 android:exported 属性必须为falseandroid: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:// + 我们声明的那个FileProviderauthorities属性值 + name属性值+ 文件名称。例如我们在文件配置路径中name的值为external-files
path:这个属性值表示要映射的子路径,例如下面的子标签的意义为


表示可以映射的路径为 Context.externalCacheDir?.path + "/images"及其子目录。什么意思呢?如果你有一个文件不在这个目录或者子目录下,对不起,映射会失败。所以有一种粗暴的做法就是将Android系统的所有目录都配置在这个文件下,那样就不会出错了,就像下面这样,本人不是太赞成这种方式。

	
	    
	    
	    
	    
	    
	

值得注意的是,从这个xml文件支持的子标签可以看出,通过FileProvider可以将存放在私有目录下的文件安全的分享给其他App。

第三步:将文件路径映射为Uri

当配置好了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)
    }		
}

第二个参数为FileProviderandroid:authorities属性值。

第四步:对获得的Uri进行授权

由于FileProviderandroid: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 提供给使用者

有主动和被动两种方式提供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,我们就可以通过ContentResolverParcelFileDescriptor 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

你可能感兴趣的:(Android)