Android開發之如何在App間安全地共享文件(FileProvider詳解)?

【版權申明】非商業目的可自由轉載
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/103125707
出自:shusheng007

相關文章
Android開發者之數據存儲,你真的會存儲數據嗎?

概述

前段時間Facebook的“隱私門”事件鬧的沸沸揚揚,可見人們對於自己的數據安全性關注度越來越高。在可預見的將來,我們的生活會越來越數字化,數據安全問題將成爲未來的頭號問題。我們可以發現,這兩年google對Android的升級主要是在安全性上做文章。Android已經過了那個野蠻生長的年代了,便利與安全問題的權衡將是未來重點。今天我們就看一下Android在App間共享文件的進化過程。

共享文件

假設有兩個App : AppProviderAppConsumer 。 AppProvider要分享自己的女朋友照片 girlfriend.jpg 給AppConsumer ,就是下面這個小姐姐。

在這裏插入圖片描述
先看一下效果圖:
在這裏插入圖片描述
上圖演示了使用系統安裝App安裝一個新的app
在這裏插入圖片描述
上圖演示了一個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聲明一下,如下代碼所示

	<application>
	    <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_provider_paths" />
        </provider>
		</application>

上面的代碼使用了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要映射的文件路徑文件

我們會發現聲明中包含了一個<meta-data>標籤。其name屬性爲固定值,而resource屬性需要一個xm文件,這個文件就是用來做路徑映射的。這個xml文件一般放在src/res/xml/路徑下,可任意命名。那它長什麼樣呢,分別代表什麼意思呢,這塊也是一個難點,反正我第一 次使用的時候沒有搞太清楚。那讓我們一起看一下它的一個例子

    <?xml version="1.0" encoding="utf-8"?>
	<paths>
	    <external-files-path
	        name="external-files"
	        path="." />
		<external-cache-path
	        name="external-cache"
	        path="images/" />
        ...
	</paths>

<paths>標籤裏面可以包括多個子標籤,每個子標籤對應Android系統中的一個文件路徑,如果對這塊不瞭解,請先閱讀Android開發者之數據存儲,你真的會存儲數據嗎?

例如上面的文件中有兩個子標籤:<external-files-path>代表context.getExternalFilesDir(null)獲取到的文件路徑,而<external-cache-path>代表context.externalCacheDir獲取到的文件路徑。

paths節點內部支持以下幾個子節點,分別爲:

<external-path/> 代表Environment.getExternalStorageDirectory()
<files-path/> 代表context.getFilesDir()
<cache-path/> 代表context.getCacheDir()
<external-files-path>代表context.getExternalFilesDirs()
<external-cache-path>代表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:這個屬性值表示要映射的子路徑,例如下面的子標籤的意義爲

<external-cache-path
       name="external-cache"
       path="images/" />

表示可以映射的路徑爲 Context.externalCacheDir?.path + "/images"及其子目錄。什麼意思呢?如果你有一個文件不在這個目錄或者子目錄下,對不起,映射會失敗。所以有一種粗暴的做法就是將Android系統的所有目錄都配置在這個文件下,那樣就不會出錯了,就像下面這樣,本人不是太贊成這種方式。

	<paths>
	    <external-path
	        name="external-path"
	        path="."/>
	    <files-path
	        name="files-path"
	        path="." />
	    <cache-path
	        name="cache"
	        path="." />
	    <external-files-path
	        name="external-files"
	        path="." />
	    <external-cache-path
	        name="external-cache"
	        path="." />
	</paths>

值得注意的是,從這個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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章