android permission 許可權與安全機制解析(下)

Shawn_Dut發表於2019-02-26

  剛建了一個QQ群,感興趣的大家一起多多交流:544645972

  在android permission許可權與安全機制解析(上)篇部落格中,我已經詳細介紹了android相關係統permission和自定義permission,以及一些許可權機制和安全機制。這篇部落格主要將會介紹到android 6.0的相關許可權更改,原理和相關的處理方式,解決方法等。

  就以我以前的一個仿最新版微信相簿為例子來分析。

android 6.0許可權全面詳細分析和解決方案

Marshmallow版本許可權修改

  android的許可權系統一直是首要的安全概念,因為這些許可權只在安裝的時候被詢問一次。一旦安裝了,app可以在使用者毫不知曉的情況下訪問許可權內的所有東西,而且一般使用者安裝的時候很少會去仔細看許可權列表,更不會去深入瞭解這些許可權可能帶來的相關危害。所以在android 6.0 Marshmallow版本之後,系統不會在軟體安裝的時候就賦予該app所有其申請的許可權,對於一些危險級別的許可權,app需要在執行時一個一個詢問使用者授予許可權。

  這裡寫圖片描述

舊版本app相容問題

  那麼問題來了,是不是所有以前釋出的app都會出現問題呢?答案是不會,只有那些targetSdkVersion 設定為23和23以上的應用才會出現異常,在使用危險許可權的時候系統必須要獲得使用者的同意才能使用,要不然應用就會崩潰,出現類似

java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaProvider複製程式碼

的崩潰日誌。所以targetSdkVersion如果沒有設定為23版本或者以上,系統還是會使用舊規則:在安裝的時候賦予該app所申請的所有許可權。所以app當然可以和以前一樣正常使用了,但是還有一點需要注意的是6.0的系統裡面,使用者可以手動將該app的許可權關閉,如下圖

  這裡寫圖片描述

  那麼問題又來了,雖然會彈出提示,但是如果以前的老應用申請的許可權被使用者不管提示強行手動關閉了怎麼辦,應用會崩潰麼?我們來試一試

  這裡寫圖片描述

  好吧,可以慶幸了一下了,不會丟擲異常,不會崩潰,只不過呼叫那些被使用者禁止許可權的api介面返回值都為null或者0,所以我們只需要做一下判空操作就可以了,不判空當然還是會崩潰的嘍。

普通許可權和危險許可權列表

  現在對於新版本的許可權變更應該有了基本的認識,那麼,是不是所有許可權都需要去進行特殊處理呢?當然不是,只有那些危險級別的許可權才需要,如列表所示:

  這裡寫圖片描述

  android開發者官網也有相關描述:

  developer.android.com/training/pe…

  developer.android.com/guide/topic…

  所以仔細去看看自己的app,對照列表,如果有需要申請其中的一個許可權,就需要進行特殊操作。還有一個比較人性的地方就是如果同一組的任何一個許可權被授權了,其他許可權也自動被授權。例如,一旦WRITE_EXTERNAL_STORAGE被授權了,app也有READ_EXTERNAL_STORAGE許可權了。

支援Marshmallow新版本許可權機制

  終於要開始支援android 6.0版本了,最先一步當然就是修改build.gradle檔案中的tragetSdkVersion和compileSdkVersion成23版本,同時使用compile ‘com.android.support:appcompat-v7:23.1.1’最新v7包。

android {
    compileSdkVersion 23
    ...

    defaultConfig {
        ...
        targetSdkVersion 23
        ...
    }
}
...
dependencies {
...
compile 'com.android.support:appcompat-v7:23.1.1'
...複製程式碼

  修改完後,感興趣的朋友可以直接打包在手機上測試一下,看看是不是會出現類似於上面我說的那些崩潰日誌。

  接著下一步當然就是要修改程式碼了,最原始程式碼,無任何處理:

private void startGetImageThread(){
....
    Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    ContentResolver contentResolver = getContentResolver();
    //獲取jpeg和png格式的檔案,並且按照時間進行倒序
    Cursor cursor = contentResolver.query(uri, null, MediaStore.Images.Media.MIME_TYPE + "=\"image/jpeg\" or " +
    MediaStore.Images.Media.MIME_TYPE + "=\"image/png\"", null, MediaStore.Images.Media.DATE_MODIFIED+" desc");
    ....
}複製程式碼

  這段程式碼需要訪問外部儲存(相簿圖片),屬於危險級別的許可權,直接使用會造成應用崩潰,所以在這段程式碼執行之前我們需要進行特殊處理:

int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
        requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
                CODE_FOR_WRITE_PERMISSION);
    return;
}複製程式碼

  寫完這段程式碼之後,就會出現如下系統dialog:

  這裡寫圖片描述

  緊接著就需要去處理DENY和ALLOW的回撥了,重寫onRequestPermissionsResult函式:

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    if (requestCode == CODE_FOR_WRITE_PERMISSION){
        if (permissions[0].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)
            &&grantResults[0] == PackageManager.PERMISSION_GRANTED){
            //使用者同意使用write
            startGetImageThread();
        }else{
            //使用者不同意,自行處理即可
            finish();
        }
    }
}複製程式碼

  好了,這樣就算是簡單初步適配完成了。

特殊許可權

  還有非常重要的兩個特殊許可權也要著重講一下,這兩個許可權很敏感,google告訴我們絕大多數應用不需要申請這兩個許可權,在Android系統中,主要有兩個:

  • SYSTEM_ALERT_WINDOW,設定懸浮窗;
  • WRITE_SETTINGS 修改系統設定
  關於上面兩個特殊許可權的授權,做法是使用 startActivityForResult 啟動授權介面來完成。

SYSTEM_ALERT_WINDOW

  想要實現一個懸浮窗,之前的解決方法很簡單,在 Manifest 檔案中新增許可權,接著設定 WindowManager.LayoutParams.type 為 WindowManager.LayoutParams.TYPE_SYSTEM_ERROR 就可以了。這種方法在 6.0 之前是可以的,但是在 M 版本之後,會如下的錯誤:

android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@83908c9 -- permission denied for this window type複製程式碼

6.0之後,google 對許可權的管理更加嚴格了,現在需要彈出懸浮框,不光要在 Manifest 中靜態申請,而且需要進行如下的動態申請:

private static final int REQUEST_CODE = 1;

private void requestAlertWindowPermission() {
    Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
    intent.setData(Uri.parse("package:" + getPackageName()));
    startActivityForResult(intent, REQUEST_CODE);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE) {
        if (Settings.canDrawOverlays(this)) {
            Log.i(LOGTAG, "onActivityResult granted");
        }
    }
}複製程式碼

上述程式碼需要注意的是

  • 使用Action Settings.ACTION_MANAGE_OVERLAY_PERMISSION 啟動隱式Intent
  • 使用 “package:” + getPackageName() 攜帶App的包名資訊
  • 使用 Settings.canDrawOverlays 方法判斷授權結果
  在使用者開啟相關許可權之後才能使用 WindowManager.LayoutParams.TYPE_SYSTEM_ERROR ,要不然是會直接崩潰的哦。

WRITE_SETTINGS

  這個許可權的用法主要是用來讀取和更改系統設定,和上一個許可權一樣是需要先在 Manifest 中靜態註冊完之後再通過下面程式碼進行動態申請:

private static final int REQUEST_CODE_WRITE_SETTINGS = 2;

private void requestWriteSettings() {
    Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
    intent.setData(Uri.parse("package:" + getPackageName()));
    startActivityForResult(intent, REQUEST_CODE_WRITE_SETTINGS);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_WRITE_SETTINGS) {
        if (Settings.System.canWrite(this)) {
            Log.i(LOGTAG, "onActivityResult write settings granted");
        }
    }
}複製程式碼

上述程式碼需要注意的是

  • 使用Action Settings.ACTION_MANAGE_WRITE_SETTINGS 啟動隱式Intent
  • 使用 “package:” + getPackageName() 攜帶App的包名資訊
  • 使用 Settings.System.canWrite 方法檢測授權結果

PACKAGE_USAGE_STATS

  這個許可權需要單獨拿出來說一下,API23版本新增,用來提供給應用手機相關元件的使用統計,該許可權也需要使用者在Setting頁面單獨授權才可使用。例如,改許可權可以用來收集最近開啟的應用,具體的可以看看android WindowManager解析與騙取QQ密碼案例分析。具體的使用方法是現在 manifest 中註冊:

複製程式碼

註冊完之後,繼續在程式碼中檢測改許可權是否被允許,如果沒有,去Setting頁面讓使用者手動開啟:

private boolean checkUsagePermission() {
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
        AppOpsManager appOps = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
        int mode = 0;
        mode = appOps.checkOpNoThrow("android:get_usage_stats", android.os.Process.myUid(), getPackageName());
        boolean granted = mode == AppOpsManager.MODE_ALLOWED;
        if (!granted) {
            Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
            startActivityForResult(intent, 1);
            return false;
        }
    }
    return true;
}
...
@TargetApi(Build.VERSION_CODES.M)
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == 1) {
        AppOpsManager appOps = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
        int mode = 0;
        mode = appOps.checkOpNoThrow("android:get_usage_stats", android.os.Process.myUid(), getPackageName());
        boolean granted = mode == AppOpsManager.MODE_ALLOWED;
        if (!granted) {
            Toast.makeText(this, "請開啟該許可權", Toast.LENGTH_SHORT).show();
        }
    }
}複製程式碼

這樣當改許可權被使用者手動開啟之後就能夠成功使用了。

處理不再提醒

  如果使用者拒絕某授權。下一次彈框,使用者會有一個“不再提醒”的選項的來防止app以後繼續請求授權。

  這裡寫圖片描述

  如果這個選項在拒絕授權前被使用者勾選了。下次為這個許可權請求requestPermissions時,對話方塊就不彈出來了,系統會直接回撥onRequestPermissionsResult函式,回撥結果為最後一次使用者的選擇。所以為了應對這種情況,系統提供了一個shouldShowRequestPermissionRationale()函式,這個函式的作用是幫助開發者找到需要向使用者額外解釋許可權的情況,這個函式:

  1. 應用安裝後第一次訪問,直接返回false;
  2. 第一次請求許可權時,使用者拒絕了,下一次shouldShowRequestPermissionRationale()返回 true,這時候可以顯示一些為什麼需要這個許可權的說明;
  3. 第二次請求許可權時,使用者拒絕了,並選擇了“不再提醒”的選項時:shouldShowRequestPermissionRationale()返回 false;
  4. 裝置的系統設定中禁止當前應用獲取這個許可權的授權,shouldShowRequestPermissionRationale()返回false;
  注意:第二次請求許可權時,才會有“不再提醒”的選項,如果使用者一直拒絕,並沒有選擇“不再提醒”的選項,下次請求許可權時,會繼續有“不再提醒”的選項,並且shouldShowRequestPermissionRationale()也會一直返回true。

  所以利用這個函式我們可以進行相應的優化,針對shouldShowRequestPermissionRationale函式返回false的處理有兩種方法:
  • 如果應用是第一次請求該許可權,則直接呼叫requestPermissions函式去請求許可權;如果不是則代表使用者勾選了’不再提醒’,彈出dialog,告訴使用者為什麼你需要該許可權,讓使用者自己手動開啟該許可權。連結:stackoverflow.com/questions/3…
  • 在onRequestPermissionsResult函式中進行檢測,如果返回PERMISSION_DENIED,則去呼叫shouldShowRequestPermissionRationale函式,如果返回false代表使用者已經禁止該許可權(上面的3和4兩種情況),彈出dialog告訴使用者你需要該許可權的理由,讓使用者手動開啟。連結:stackoverflow.com/questions/3…
  處理方法已經有了,修改一下程式碼,我這裡就以第二種方案來處理了:

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    if (requestCode == CODE_FOR_WRITE_PERMISSION){
        if (permissions[0].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)
            &&grantResults[0] == PackageManager.PERMISSION_GRANTED){
            //使用者同意使用write
            startGetImageThread();
        }else{
            //使用者不同意,向使用者展示該許可權作用
            if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                AlertDialog dialog = new AlertDialog.Builder(this)
                        .setMessage("該相簿需要賦予訪問儲存的許可權,不開啟將無法正常工作!")
                        .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                finish();
                            }
                        })
                        .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                finish();
                            }
                        }).create();
                dialog.show();
                return;
            }
            finish();
        }
    }
}複製程式碼

  當勾選不再提醒,並且拒絕之後,彈出dialog,提醒使用者該許可權的重要性:

  這裡寫圖片描述

  搞定!!!

使用相容庫

  以上的程式碼在6.0版本上使用沒有問題,但是在之前就有問題了,最簡單粗暴的解決方法可能就是利用Build.VERSION.SDK_INT >= 23這個判斷語句來判斷了,方便的是SDK 23的v4包加入了專門類進行相關的處理:

  • ContextCompat.checkSelfPermission()
  • 被授權函式返回PERMISSION_GRANTED,否則返回PERMISSION_DENIED ,在所有版本都是如此。
  • ActivityCompat.requestPermissions()
  • 這個方法在6.0之前版本呼叫,OnRequestPermissionsResultCallback 直接被呼叫,帶著正確的 PERMISSION_GRANTED或者PERMISSION_DENIED。
  • ActivityCompat.shouldShowRequestPermissionRationale()
  • 在6.0之前版本呼叫,永遠返回false。
  用v4包的這三方法,完美相容所有版本!下面是程式碼:

//使用相容庫就無需判斷系統版本
int hasWriteContactsPermission = ContextCompat.checkSelfPermission(getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (hasWriteContactsPermission == PackageManager.PERMISSION_GRANTED) {
    startGetImageThread();
}
//需要彈出dialog讓使用者手動賦予許可權
else{
    ActivityCompat.requestPermissions(PickOrTakeImageActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CODE_FOR_WRITE_PERMISSION);
}複製程式碼

  onRequestPermissionsResult函式不變。後兩個方法,我們也可以在Fragment中使用,用v13相容包:FragmentCompat.requestPermissions() and FragmentCompat.shouldShowRequestPermissionRationale()和activity效果一樣。

一次請求多個許可權

  當然了有時候需要多個許可權,可以用上面方法一次請求多個許可權。當然最重要的是不要忘了為每個許可權檢查“不再提醒”的設定。

List permissionsNeeded = new ArrayList();
permissionsNeeded.add(Manifest.permission.ACCESS_FINE_LOCATION);
permissionsNeeded.add(Manifest.permission.READ_CONTACTS);
permissionsNeeded.add(Manifest.permission.WRITE_CONTACTS);
requestPermissions(permissionsNeeded.toArray(new String[permissionsList.size()]), CODE_FOR_MULTIPLE_PERMISSION);複製程式碼

  最後在onRequestPermissionsResult函式中一個個處理返回結果即可。

第三方庫簡化程式碼

  當然早就有第三方庫來幫忙做這些事情了:

  Github上的開源專案 PermissionHelperhotchemi’s PermissionsDispatcher

APP處於執行狀態下,被撤銷許可權

  如果APP正在執行中,使用者進入設定-應用程式頁面去手動撤銷該APP許可權,會出現什麼情況呢?哈哈,系統又會接著彈出許可權請求對話方塊,挺好挺好:

  這裡寫圖片描述

  這樣就沒有問題了吧O(∩_∩)O~

  上面的測試環境為genymotion6.0模擬器,有朋友跟我反映在6.0nexus 6p真機上會直接退出應用,所以這個應該還和測試環境有關。

結論建議

  新執行時許可權已經在棉花糖中被使用了。我們沒有退路。我們現在唯一能做的就是保證app適配新許可權模型。欣慰的是隻有少數許可權需要執行時許可權模型。大多數常用的許可權,例如,網路訪問,屬於Normal Permission 在安裝時自動會授權,當然你要宣告,以後無需檢查。因此,只有少部分程式碼你需要修改。

兩個建議:

  1.嚴肅對待新許可權模型。

  2.如果你程式碼沒支援新許可權,不要設定targetSdkVersion 23 。尤其是當你在Studio新建工程時,不要忘了修改!

  說一下程式碼修改。這是大事,如果程式碼結構被設計的不夠好,你需要一些很蛋疼的重構。每個app都要被修正。如上所說,我們沒的選擇。列出所有你需要請求許可權的全部情形,如果A被授權,B被拒絕,會發生什麼,針對每一個情況認真處理。

引用文章


www.jianshu.com/p/e1ab1a179…

blog.csdn.net/yangqingqo/…

inthecheesefactory.com/blog/things…

developer.android.com/training/pe…

developer.android.com/guide/topic…

developer.android.com/reference/a…

www.open-open.com/lib/view/op…


相關文章