系統(tǒng)應(yīng)用根據(jù)Uri授予權(quán)限方法詳解
系統(tǒng)應(yīng)用根據(jù)Uri授予權(quán)限的正確姿勢(shì)
在我們印象中,Android6.0以后訪問(wèn)外部的媒體資源文件都是需要申請(qǐng)READ_EXTERNAL_STORAGE
才可以正常訪問(wèn),思考一個(gè)場(chǎng)景,假如我們不申請(qǐng)?jiān)摍?quán)限,使用系統(tǒng)的Intent.ACTION_PICK
意圖跳轉(zhuǎn)系統(tǒng)圖庫(kù)選取圖片是否可以正常顯示該圖片?
答案是可以的,這是為什么呢?我們都沒(méi)有申請(qǐng)權(quán)限,或者說(shuō)是誰(shuí)給了我們這個(gè)權(quán)限?帶著這個(gè)疑問(wèn)我們先來(lái)了解下UriPermission
UriPermission
Allow access on a per-URI basis
You can also grant permissions on a per-URI basis. When starting an activity or returning a result to an activity, set the Intent.FLAG_GRANT_READ_URI_PERMISSION, intent flag, the Intent.FLAG_GRANT_WRITE_URI_PERMISSION, intent flag, or both flags. This gives another app read, write, and read/write permissions, respectively, for the data URI that's included in the intent. The other app gains these permissions for the specific URI regardless of whether it has permission to access data in the content provider more generally.
上面是Android官網(wǎng)的解釋?zhuān)蟾乓馑际?,你可以進(jìn)一步對(duì)其他應(yīng)用如何訪問(wèn)你應(yīng)用的Contete Provider
或者數(shù)據(jù)URI
進(jìn)行精細(xì)控制,可以通過(guò)讀寫(xiě)權(quán)限來(lái)保護(hù)自己,根據(jù) URI 授予權(quán)限。
在啟動(dòng) activity 或?qū)⒔Y(jié)果返回給 activity 時(shí),請(qǐng)?jiān)O(shè)置 Intent.FLAG_GRANT_READ_URI_PERMISSION
intent 標(biāo)志、Intent.FLAG_GRANT_WRITE_URI_PERMISSION
intent 標(biāo)志或者同時(shí)設(shè)置兩者。
這樣便可針對(duì) intent 中包含的數(shù)據(jù) URI 分別向另一個(gè)應(yīng)用授予讀取權(quán)限、寫(xiě)入權(quán)限和讀寫(xiě)權(quán)限。
理解完UriPermission,上面的問(wèn)題就可以解釋了,雖然我們沒(méi)有申請(qǐng)讀取的權(quán)限,但是系統(tǒng)圖庫(kù)在選圖后將結(jié)果返回activity時(shí)設(shè)置了Intent.FLAG_GRANT_READ_URI_PERMISSION
,這樣我們就具有了讀取該Uri指定圖片的權(quán)限。
理想很豐滿,現(xiàn)實(shí)很骨感。
背景
最近在項(xiàng)目中上線一款自研的圖庫(kù)應(yīng)用(systemUid),支持系統(tǒng)默認(rèn)選圖action跳轉(zhuǎn),給調(diào)用者返回已選的Uri資源地址,因?yàn)榘踩弦?guī)整改的原因,一些第三方應(yīng)用去掉了讀寫(xiě)權(quán)限的申請(qǐng),問(wèn)題就被暴露出來(lái)了,第三方應(yīng)用無(wú)法正常通過(guò)圖庫(kù)獲取到圖片資源。
分析
分析堆棧信息可以定位到,是調(diào)用者沒(méi)有訪問(wèn)Uri的權(quán)限導(dǎo)致的異常,而我們自研圖庫(kù)在選圖回傳的時(shí)候是有設(shè)置FLAG_GRANT_READ_URI_PERMISSION,把URI的臨時(shí)訪問(wèn)權(quán)限傳遞給調(diào)用者,且報(bào)錯(cuò)的堆棧打印是在第三方應(yīng)用,所以可以初步判斷問(wèn)題應(yīng)該是出自系統(tǒng)的權(quán)限授予過(guò)程。
我們可以通過(guò)context.grantUriPermission()
作為切入點(diǎn),來(lái)分析下系統(tǒng)是如何授予Uri權(quán)限
最終調(diào)用的是UriGrantsManagerService$checkGrantUriPermission()
checkGrantUriPermission
int checkGrantUriPermission(int callingUid, String targetPkg, GrantUri grantUri, final int modeFlags, int lastTargetUid) { .... // Bail early if system is trying to hand out permissions directly; it // must always grant permissions on behalf of someone explicit. final int callingAppId = UserHandle.getAppId(callingUid); if ((callingAppId == SYSTEM_UID) || (callingAppId == ROOT_UID)) { if ("com.android.settings.files".equals(grantUri.uri.getAuthority()) || "com.android.settings.module_licenses".equals(grantUri.uri.getAuthority())) { // Exempted authority for // 1. cropping user photos and sharing a generated license html // file in Settings app // 2. sharing a generated license html file in TvSettings app // 3. Sharing module license files from Settings app } else { Slog.w(TAG, "For security reasons, the system cannot issue a Uri permission" + " grant to " + grantUri + "; use startActivityAsCaller() instead"); return -1; } } .... } 復(fù)制代碼
問(wèn)題就是出現(xiàn)在這里,如果你的應(yīng)用是root用戶,或者是具有系統(tǒng)級(jí)權(quán)限(systemUid),并且提供的Uri的authority不是指定的,就會(huì)拒絕授權(quán) (return -1),也就是說(shuō)這個(gè)Uri的權(quán)限并沒(méi)有傳遞成功。
這一點(diǎn)在上面的日志也有體現(xiàn)。
/system_process W UriGrantsManagerService: For security reasons, the system cannot issue a Uri permission grant to
系統(tǒng)為什么要這么做?
注釋里面有說(shuō),出于安全原因,系統(tǒng)應(yīng)用不能使用startActivityAsCaller()來(lái)直接授予Uri權(quán)限,它必須顯式地讓?xiě)?yīng)用自己授予權(quán)限。只有以下幾種情況才會(huì)豁免授權(quán)
- 裁剪用戶照片并共享生成的許可HTML文件的設(shè)置應(yīng)用程序
- 在TvSettings app中共享生成的許可html文件
- 從設(shè)置應(yīng)用程序共享模塊license文件
調(diào)用者端
分析完圖庫(kù)端,我們?cè)賮?lái)看下調(diào)用者端的異常堆棧打印
客戶端遠(yuǎn)程調(diào)用服務(wù)端打開(kāi)指定文件,然后服務(wù)端把文件描述符跨進(jìn)程傳遞到客戶端(后面Binder驅(qū)動(dòng)跨進(jìn)程傳遞文件描述符就不展開(kāi)分析)
通過(guò)分析時(shí)序圖,可以定位到報(bào)錯(cuò)的堆棧信息是發(fā)生在enforceCallingPermissionInternal()
enforceCallingPermissionInternal()
private void enforceCallingPermissionInternal(Uri uri, boolean forWrite) { ... // 省略部分代碼 // First, check to see if caller has direct write access if (forWrite) { final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, uri, table, null); try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) { if (c.moveToFirst()) { // Direct write access granted, yay! return; } } } ... // 省略部分代碼 // Second, check to see if caller has direct read access final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, uri, table, null); try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) { if (c.moveToFirst()) { if (!forWrite) { // Direct read access granted, yay! return; } else if (allowUserGrant) { // Caller has read access, but they wanted to write, and // they'll need to get the user to grant that access final Context context = getContext(); final PendingIntent intent = PendingIntent.getActivity(context, 42, new Intent(null, uri, context, PermissionActivity.class), FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE); final Icon icon = getCollectionIcon(uri); final RemoteAction action = new RemoteAction(icon, context.getText(R.string.permission_required_action), context.getText(R.string.permission_required_action), intent); throw new RecoverableSecurityException(new SecurityException( getCallingPackageOrSelf() + " has no access to " + uri), context.getText(R.string.permission_required), action); } } } throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri); }
在這個(gè)方法里面,它會(huì)根據(jù)參數(shù)forWrite
判斷當(dāng)前調(diào)用者是否擁有讀寫(xiě)權(quán)限,如果沒(méi)有則會(huì)拋出異常提示,和上面報(bào)錯(cuò)的異常堆棧符合。
解決辦法
以下兩種方法都可以
- 去除應(yīng)用systemUid配置
- 修改framework源碼
考慮到修改系統(tǒng)源碼的影響面比較大,所以采用去除systemUid的方式,再次驗(yàn)證跳轉(zhuǎn)選圖后可以正常加載。查看系統(tǒng)原生圖庫(kù)的清單文件配置,也是沒(méi)有設(shè)置systemUid,原生圖庫(kù)也是采用了這種方式。
擴(kuò)展
系統(tǒng)原生圖庫(kù)既不是系統(tǒng)應(yīng)用,也沒(méi)有動(dòng)態(tài)申請(qǐng)存儲(chǔ)權(quán)限,那它是怎么獲取系統(tǒng)的存儲(chǔ)權(quán)限的?
如果有了解過(guò)系統(tǒng)權(quán)限的授予流程,可以知道Android系統(tǒng)在開(kāi)機(jī)后會(huì)對(duì)一些特殊的應(yīng)用進(jìn)行自動(dòng)授權(quán),而運(yùn)行時(shí)權(quán)限的默認(rèn)授予工作由DefaultPermissionGrantPolicy類(lèi)的grantDefaultPermissions方法完成。
在這個(gè)方法里面可以看到它對(duì)圖庫(kù)進(jìn)行默認(rèn)授予存儲(chǔ)權(quán)限的代碼,具體是通過(guò)查找清單文件配置的category是Intent.CATEGORY_APP_GALLERY的應(yīng)用。
以上就是系統(tǒng)應(yīng)用根據(jù)Uri授予權(quán)限方法詳解的詳細(xì)內(nèi)容,更多關(guān)于系統(tǒng)應(yīng)用Uri授權(quán)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android切換至SurfaceView時(shí)閃屏(黑屏閃一下)以及黑屏移動(dòng)問(wèn)題的解決方法
本文主要介紹了Android切換至SurfaceView時(shí)閃屏(黑屏閃一下)以及黑屏移動(dòng)問(wèn)題的解決方法。具有一定的參考作用,下面跟著小編一起來(lái)看下吧2017-01-01Android實(shí)現(xiàn)調(diào)用手機(jī)攝像頭錄像限制錄像時(shí)長(zhǎng)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)調(diào)用手機(jī)攝像頭錄像限制錄像時(shí)長(zhǎng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03Android編程之退出整個(gè)應(yīng)用程序的方法
這篇文章主要介紹了Android編程之退出整個(gè)應(yīng)用程序的方法,實(shí)例分析了Android直接關(guān)閉所有的Acitivity并退出應(yīng)用程序的實(shí)現(xiàn)技巧,需要的朋友可以參考下2015-12-12android輸入框內(nèi)容改變的監(jiān)聽(tīng)事件實(shí)例
下面小編就為大家分享一篇android輸入框內(nèi)容改變的監(jiān)聽(tīng)事件實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-02-02淺談Android RecyclerView UI的滾動(dòng)控件示例
本篇文章主要介紹了淺談Android RecyclerView UI的滾動(dòng)控件示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02Android自定義ListView實(shí)現(xiàn)下拉刷新上拉加載更多
Listview現(xiàn)在用的很少了,基本都是使用Recycleview,但是不得不說(shuō)Listview具有劃時(shí)代的意義,我們可以自己添加下拉刷新,上拉加載更多功能。本文就來(lái)利用自定義ListView實(shí)現(xiàn)下拉刷新上拉加載更多效果,需要的可以參考一下2022-10-10解決Android Studio日志太長(zhǎng)或滾動(dòng)太快問(wèn)題
這篇文章主要介紹了解決Android Studio日志太長(zhǎng)或滾動(dòng)太快問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-04-04android 實(shí)現(xiàn)類(lèi)似微信緩存和即時(shí)更新好友頭像示例
本篇文章主要介紹了android 實(shí)現(xiàn)類(lèi)似微信緩存和即時(shí)更新好友頭像示例,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-01-01android nfc常用標(biāo)簽讀取總結(jié)
NFC(Near Field Communication,近場(chǎng)通信)是一種數(shù)據(jù)傳輸技術(shù)這篇文章主要介紹了android nfc常用標(biāo)簽讀取總結(jié),有興趣的可以了解一下。2016-12-12