【版權申明】非商業目的可自由轉載
博文地址: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之前
在Android7.0之前,AppProvider 需要先把這張圖片分享給AppConsumer 需要做如下幾步:
- 將圖片放到文件系統的非私有目錄下
- 將圖片訪問權限設置爲可讀
- 將圖片的地址告訴AppConsumer
- 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
屬性必須爲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
要映射的文件路徑文件
我們會發現聲明中包含了一個<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:// + 我們聲明的那個FileProvider
的 authorities屬性值 + 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)
}
}
第二個參數爲FileProvider
的android:authorities
屬性值。
第四步:對獲得的Uri進行授權
由於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 提供給使用者
有主動和被動兩種方式提供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