簡述
Android是一個權(quán)限分離的操作系統(tǒng),每一個應(yīng)用程序運行時都會有一個明確地系統(tǒng)身份標(biāo)識(Linux的user ID和group ID)。部分系統(tǒng)也同樣被特定身份標(biāo)識而隔開。因此,Linux才能將應(yīng)用程序與其他程序和系統(tǒng)隔離開來。
這樣的機制可以說是相當(dāng)安全,但是也阻斷了各個應(yīng)用程序之間或者和系統(tǒng)之間的“交流”。因此,Android通過一種“permission”機制強力限制某些特定地操作來達(dá)到細(xì)粒度的安全能力。
進(jìn)程沙箱
Android進(jìn)程沙箱機制是借鑒Linux中用戶組的原理,其限制了不同應(yīng)用程序之間的資源和數(shù)據(jù)的互訪。當(dāng)應(yīng)用首次安裝的時,系統(tǒng)會向其分配一個UID。如果該應(yīng)用程序是第三方的,那么其UID值大于10000,如果是系統(tǒng)應(yīng)用程序則小于10000。如果應(yīng)用程序卸載后又重新安裝,那么其UID值是會改變的。
//獲取應(yīng)用程序UID方法
public void getApplicationUid() {
PackageManager pm = getPackageManager();
try {
ApplicationInfo ai = pm.getApplicationInfo(getPackageName(), PackageManager.GET_ACTIVITIES);
Log.d(getClass().getSimpleName(), "uid = " + ai.uid);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
不同UID的應(yīng)用程序是不能進(jìn)行資源互訪,從而有效達(dá)到進(jìn)程隔離目的。

此外,你可以每個應(yīng)用程序的AndroidManifest.xml文件中使用ShareUserID屬性來使他們擁有同一UserID。UserID相同的應(yīng)用程序?qū)幌到y(tǒng)當(dāng)做同一應(yīng)用程序,擁有相同的UserID和文件權(quán)限。
注意:為了保留系統(tǒng)安全性,只有簽名相同(并且需要相同的shareUserId)的應(yīng)用程序才會被分配相同的UserID。
一個應(yīng)用程序存儲的任何數(shù)據(jù)都會被分配應(yīng)用程序的UserID,通常是不能被其他應(yīng)用程序所訪問。當(dāng)使用getSharedPreferences(String, int),openFileOutput(String, int),或者openOrCreateDatabase(String, int, SQLiteDatabase.CursorFactory),你可以使用MODE_WORLD_READABLE或者M(jìn)ODE_WORLD_WRITEABLE標(biāo)記來允許其他應(yīng)用程序讀或?qū)懳募?/p>
權(quán)限使用
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.app.myapp" >
<uses-permission android:name="android.permission.RECEIVE_SMS" />
...
</manifest>
權(quán)限的使用是相當(dāng)簡單的,某功能需要申請權(quán)限時,只需在AndroidManifest.xml文件中申明對應(yīng)權(quán)限就行。如上述代碼。
如果你的App在其manifest文件中聲明一系列normal permissions(不會對用戶隱私或者設(shè)備運行構(gòu)成威脅的權(quán)限),系統(tǒng)會自動準(zhǔn)許這些權(quán)限申請。如果你的App在其manifest文件中聲明一系列dangerous permissions(對用戶隱私或者設(shè)備運行構(gòu)成潛在威脅的權(quán)限),系統(tǒng)將會詢問用戶是否同意這些權(quán)限申請。
詢問的方式根據(jù)系統(tǒng)的版本而有所不同。
1.靜態(tài)權(quán)限申請詢問界面
若設(shè)備運行的系統(tǒng)版本為Android5.1(API版本22)或更低,或者App的targetSdkVersion是22或更低,Android提供的是靜態(tài)權(quán)限申請詢問界面。
這種詢問方式只要玩過Android手機的應(yīng)該都見過,當(dāng)應(yīng)用程序首次安裝時,會彈出以下類似界面,出現(xiàn)在圖標(biāo)列表中的權(quán)限都是dangerous permissions。

這種詢問方式相當(dāng)霸道,如果想要安裝該應(yīng)用,我們只有同意其申請的所有權(quán)限。當(dāng)應(yīng)用程序安裝更新時,如果該應(yīng)用程序有新申請的權(quán)限,那么該權(quán)限詢問界面會將新申請的權(quán)限列出。你廢除這些權(quán)限申請的唯一方式就是卸載它們!
-
動態(tài)權(quán)限申請詢問界面
如果設(shè)備運行的系統(tǒng)版本為Android6.0(API版本23)或更高,或者App的targetSdkVersion是23或更高,Android提供了動態(tài)權(quán)限申請詢問界面。
其實這種方式,早在Android6.0之前就有大批國產(chǎn)ROM提供動態(tài)權(quán)限管理方式,市面上主流的安全軟件也提供這種功能。Google終于在Android6.0提供了動態(tài)權(quán)限管理功能(不過對我大天朝來說然并卵)。
動態(tài)權(quán)限申請詢問界面
這種交互方式更加的人性化,也更加安全。在應(yīng)用程序運行的過程中,如果需要申請網(wǎng)絡(luò)連接權(quán)限,那么系統(tǒng)會彈出權(quán)限詢問對話框供用戶選擇。
當(dāng)然,權(quán)限并不僅僅局限于此。我們也可以自定義某些權(quán)限來保證安全性。比如,啟動Activity或者Service時,增加權(quán)限控制,防止被外部應(yīng)用程序胡亂啟動。
權(quán)限組
對普通第三方應(yīng)用程序來說,權(quán)限一般分為normal permission和dangerous permission。Android系統(tǒng)所有的dangerous permissions都屬于某一權(quán)限組。如果設(shè)備運行的系統(tǒng)版本為Android6.0(API版本23)或更高,或者App的targetSdkVersion是23或更高,當(dāng)你的應(yīng)用程序需要一個dangerous permission時,那么:
- 如果應(yīng)用程序在manifest中聲明了一個dangerous permission,并且它目前沒有該權(quán)限組中的任一權(quán)限,那么系統(tǒng)會彈出一個將要申請權(quán)限組的對話框。但是該對話框不會具體描述是該權(quán)限組中的哪一個權(quán)限。比如應(yīng)用程序需要READ_CONTACTS權(quán)限,那么該對話框僅僅只描述為該應(yīng)用程序需要訪問聯(lián)系人。
2.如果應(yīng)用程序在manifest中聲明了一個dangerous permission,并且它已經(jīng)擁有該權(quán)限組的其他權(quán)限,那么系統(tǒng)將直接允許其訪問該權(quán)限,不與用戶產(chǎn)生交互。
若設(shè)備運行的系統(tǒng)版本為Android5.1(API版本22)或更低,或者App的targetSdkVersion是22或更低,系統(tǒng)將會在應(yīng)用程序安裝的時候讓用戶同意權(quán)限申請。系統(tǒng)僅僅只告訴用戶哪些權(quán)限組被申請,而不是單獨某一個權(quán)限。
Android6.0動態(tài)權(quán)限管理
國產(chǎn)ROM和各類安全軟件早已提供了動態(tài)權(quán)限管理功能,實現(xiàn)方式上大同小異,雖然對用戶來說這是相當(dāng)利好的消息,但是對我們開發(fā)者來說,還是很麻煩的,各種ROM的兼容性讓我們很頭疼。終于在棉花糖上,Android提供了動態(tài)權(quán)限管理的相關(guān)API,我們在處理權(quán)限問題上方便了很多。
當(dāng)你的需要申請一個dangerous permission時候,你必須在每次申請之前進(jìn)行權(quán)限檢查。權(quán)限檢查的方法如下。
int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE);
如果該方法的返回值為PackageManager.PERMISSION_GRANTED,應(yīng)用程序就可以繼續(xù)后續(xù)操作。如果應(yīng)用程序沒有該權(quán)限,那么方法的返回值為PERMISSION_DENIED,并且將會詢問用戶是否允許該權(quán)限。
我們在Manifest中申請的任何dangerous permission,都會詢問用戶是否允許該權(quán)限,Android提供了幾個申請權(quán)限的方法,調(diào)用之后,會彈出一個標(biāo)準(zhǔn)的系統(tǒng)對話框供用戶選擇,該對話框是不能自定義的。
如果一個圖像類軟件申請發(fā)短信權(quán)限,用戶可能會產(chǎn)生懷疑,是不是扣費短信。那么我們?nèi)绾谓档陀脩舻牟乱赡??Android提供了一個比較實用的方法shouldShowRequestPermissionRationale(),該方法給了我們一個解釋的機會來增加權(quán)限申請通過的概率。如果該權(quán)限之前已被申請過但是被用戶拒絕,那么shouldShowRequestPermissionRationale()方法返回true。
如果你的應(yīng)用程序沒有所需要的權(quán)限,那么你必須要通過調(diào)用requestPermissions()方法來申請權(quán)限,該方法調(diào)用后,系統(tǒng)會立刻彈出權(quán)限申請詢問對話框供供用戶選擇,在用戶交互后,系統(tǒng)會立刻通過onRequestPermissionsResult()將結(jié)果返回給應(yīng)用程序。這里直接將官方文檔中相關(guān)演示代碼貼出來供參考。
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS)) {
// Show an expanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
} else {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the
// contacts-related task you need to do.
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
// other 'case' lines to check for other
// permissions this app might request
}
}
再一次,系統(tǒng)權(quán)限對話框僅僅只描述你所申請權(quán)限所在權(quán)限組的描述,而不是針對某一特定權(quán)限。對于同一權(quán)限組的權(quán)限,用戶只需同意一次即可。這種方案的好壞見仁見智,但是有時候感覺會把一個很小的問題給擴大了,比如我們只是需要簡單的獲取設(shè)備的IMEI碼,那么這時候系統(tǒng)對話框的描述為應(yīng)用程序?qū)⒃L問設(shè)備信息。這時候用戶肯定會想,你訪問我設(shè)備信息作甚!然后你的申請被無情拒絕了!
注意:你的應(yīng)用程序需要明確地申請每一個你需要的權(quán)限,即使用戶已經(jīng)同意了該權(quán)限所在權(quán)限組的另外一個權(quán)限。此外,隨著Android版本的更新,權(quán)限組中所含的權(quán)限可能會改變。因此,不要偷懶,該顯示申請權(quán)限的地方還是要乖乖申請吧。
合理申請權(quán)限
曾幾何時,權(quán)限的濫用導(dǎo)致用戶隱私泄露頻發(fā),而今,用戶對隱私也愈發(fā)敏感,過渡的權(quán)限申請會給用戶造成不良的印象。因此,作為有節(jié)操的程序員,我們在權(quán)限申請上應(yīng)該慎重,而不是一股腦把所有權(quán)限都給申請。
隨著Android版本的更新,相應(yīng)的權(quán)限也會更新,因此我們一定要注意不同targetSdkVersion屬性所帶來的權(quán)限變化,并盡可能的提高targetSdkVersion。在權(quán)限使用上Google也給了我們一些建議。
- 考慮使用Intent來完成權(quán)限相關(guān)的操作
這點建議,我覺得可以作為一個比較好的參考。在Manifest中,我們申請了SEND_SMS權(quán)限,那么可以通過下面代碼完成發(fā)送短信功能。
SmsManager sm = SmsManager.getDefault();
sm.sendTextMessage(address, null, message, null, null);
如果發(fā)送短信時候,用戶選擇拒絕該權(quán)限申請,那么你的功能也就Over了。
如果我們換intent方式進(jìn)行發(fā)送短信,則不會出現(xiàn)權(quán)限被拒絕的情況,代碼如下。
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("smsto:" + number));
intent.putExtra("sms_body", body);
context.startActivity(sendIntent);
該方法會跳轉(zhuǎn)到發(fā)送短信界面(如果系統(tǒng)裝有多個短信類應(yīng)用,那么系統(tǒng)會彈出一個選擇應(yīng)用對話框,讓用戶選擇使用何種應(yīng)用來完成發(fā)送短信功能),并填充好相應(yīng)的內(nèi)容。類似的,撥打電話和使用照相機等都可以使用intent來完成相應(yīng)的功能,降低了用戶拒絕權(quán)限的風(fēng)險。
最后,權(quán)限方式和intent方式各有千秋,根據(jù)不同的業(yè)務(wù)情景,我們可以選擇不同的方式。
- 只申請你所需要的權(quán)限
不想讓用戶覺得你的應(yīng)用程序是一個“流氓應(yīng)用”,最好不要過度申請權(quán)限。
3.不要“吞噬”用戶
在Android6.0中,不要在同一時刻申請多種權(quán)限。因為系統(tǒng)可能會彈出多個系統(tǒng)權(quán)限詢問對話框,這種情況:
第一,用戶可能覺得很煩鎖,并退出你的應(yīng)用程序。
第二,用戶可能由于誤操作,拒絕了你的某些權(quán)限申請。
因此,最好的方式還是在你需要的時候進(jìn)行申請吧。
4.給出你為什么使用權(quán)限的原因
為了降低權(quán)限申請被拒絕的風(fēng)險,最好在調(diào)用requestPermissions()之前,進(jìn)行權(quán)限申請的說明,使用戶覺得你不是在做“壞事”。
動態(tài)權(quán)限申請的一種解決方案
雖然目前Android6.0市場占有率相當(dāng)?shù)停请S著時間的推移,關(guān)于動態(tài)權(quán)限管理這一塊,我們遲早要接觸的。這里我參考Android官方開發(fā)文檔,封裝了動態(tài)權(quán)限管理所需的方法。雖然自己的項目中還未用到動態(tài)權(quán)限管理,但作為工作之余的學(xué)習(xí)還是大有裨益!
權(quán)限申請流程

BaseActivity中完成權(quán)限申請
這里我沒有將權(quán)限申請相關(guān)方法封裝成一個類,而是在BaseActivity中添加相關(guān)方法。
public class BaseActivity extends AppCompatActivity {
//申請請求的request code
private final static int YZT_PERMISSION_REQUEST = 12;
public final String TAG = getClass().getSimpleName();
//是否跳轉(zhuǎn)過應(yīng)用程序信息詳情頁
private boolean mIsJump2Settings = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
if (mIsJump2Settings) {
onRecheckPermission();
mIsJump2Settings = false;
}
}
//單個權(quán)限的檢查
public void checkPermission(@NonNull final String permission, @Nullable String reason) {
if (Build.VERSION.SDK_INT < 23) return;
int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
//權(quán)限已經(jīng)申請
onPermissionGranted(permission);
} else {
if (!TextUtils.isEmpty(reason)) {
//判斷用戶先前是否拒絕過該權(quán)限申請,如果為true,我們可以向用戶解釋為什么使用該權(quán)限
if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
//這里的dialog可以自定義
new AlertDialog.Builder(this).setCancelable(false).setTitle("溫馨提示").setMessage(reason).
setNegativeButton("我知道了", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermission(new String[]{permission});
dialog.dismiss();
}
}).show();
} else {
requestPermission(new String[]{permission});
}
} else {
requestPermission(new String[]{permission});
}
}
}
//多個權(quán)限的檢查
public void checkPermissions(@NonNull String... permissions) {
if (Build.VERSION.SDK_INT < 23) return;
//用于記錄權(quán)限申請被拒絕的權(quán)限集合
List<String> permissionDeniedList = new ArrayList<>();
for (String permission : permissions) {
int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
onPermissionGranted(permission);
} else {
permissionDeniedList.add(permission);
}
}
if (!permissionDeniedList.isEmpty()) {
String[] deniedPermissions = permissionDeniedList.toArray(new String[permissionDeniedList.size()]);
requestPermission(deniedPermissions);
}
}
//調(diào)用系統(tǒng)API完成權(quán)限申請
private void requestPermission(String[] permissions) {
ActivityCompat.requestPermissions(this, permissions, YZT_PERMISSION_REQUEST);
}
//申請權(quán)限被允許的回調(diào)
public void onPermissionGranted(String permission) {
}
//申請權(quán)限被拒絕的回調(diào)
public void onPermissionDenied(String permission) {
}
//申請權(quán)限的失敗的回調(diào)
public void onPermissionFailure() {
}
//如果從設(shè)置界面返回,則重新申請權(quán)限
public void onRecheckPermission() {
}
//彈出系統(tǒng)權(quán)限詢問對話框,用戶交互后的結(jié)果回調(diào)
@Override
public final void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case YZT_PERMISSION_REQUEST:
if (grantResults.length > 0) {
//用于記錄是否有權(quán)限申請被拒絕的標(biāo)記
boolean isDenied = false;
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
onPermissionGranted(permissions[i]);
} else {
isDenied = true;
onPermissionDenied(permissions[i]);
}
}
if (isDenied) {
isDenied = false;
//如果有權(quán)限申請被拒絕,則彈出對話框提示用戶去修改權(quán)限設(shè)置。
showPermissionSettingsDialog();
}
} else {
onPermissionFailure();
}
break;
}
}
private void showPermissionSettingsDialog() {
new AlertDialog.Builder(this).setCancelable(false).setTitle("溫馨提示").
setMessage("缺少必要權(quán)限\n不然將導(dǎo)致部分功能無法正常使用").setNegativeButton("下次吧", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
}).setPositiveButton("去設(shè)置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
jump2PermissionSettings();
}
}).show();
}
/**
* 跳轉(zhuǎn)到應(yīng)用程序信息詳情頁面
*/
private void jump2PermissionSettings() {
mIsJump2Settings = true;
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
}
使用方法
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//權(quán)限檢查
String[] permissionArray = {Manifest.permission.SEND_SMS, Manifest.permission.CALL_PHONE};
checkPermissions(permissionArray);
// checkPermission(Manifest.permission.SEND_SMS, "YZT將要發(fā)生短信進(jìn)行身份驗證");
}
@Override
public void onRecheckPermission() {
super.onRecheckPermission();
String[] permissionArray = {Manifest.permission.SEND_SMS, Manifest.permission.CALL_PHONE};
checkPermissions(permissionArray);
}
@Override
public void onPermissionGranted(String permission) {
super.onPermissionGranted(permission);
switch (permission) {
case Manifest.permission.SEND_SMS:
//TODO:發(fā)送短信
Toast.makeText(this, "發(fā)短信咯", Toast.LENGTH_LONG).show();
break;
case Manifest.permission.CALL_PHONE:
//TODO:打電話
Toast.makeText(this, "電話咯", Toast.LENGTH_LONG).show();
break;
}
}
@Override
public void onPermissionDenied(String permission) {
super.onPermissionDenied(permission);
switch (permission) {
case Manifest.permission.SEND_SMS:
//TODO:
break;
case Manifest.permission.CALL_PHONE:
//TODO:
break;
}
}
@Override
public void onPermissionFailure() {
super.onPermissionFailure();
Toast.makeText(this, "權(quán)限獲取失敗", Toast.LENGTH_LONG).show();
}
隨著用戶安全意識的提升,我們在權(quán)限的使用上也應(yīng)該更加趨于合理和謹(jǐn)慎。雖然目前Android6.0的占有率很低,但是我們也應(yīng)該未雨綢繆,盡快引入動態(tài)權(quán)限管理機制。
