本篇部分内容引用自官网介绍 系统权限
一.什么是权限?
1.简介
Android 是一个权限分隔的操作系统,每个应用在操作系统中都有一个属于自己的标识(Linux 的用户 ID ).Linux 据此将不同的应用以及应用与系统分隔开来。
在涉及到更加详细的功能的时候,比如对可以对某个组件进行访问,的时候可以通过权限机制进行特定的操作,并且可以通过 URI 权限授权临时的访问。
2.Android 的安全架构
Android 的架构设计的一个特点是:在默认的情况下任何应用都没有权限执行对其他应用,操作系统或者用户不利影响的任何操作。包括读取或者写入用户的的私有数据(读取用户手机联系人或者相册),读取或者写入其他应用程序的文件,执行网络访问等。
但是有的时候应用需要显示地共享资源和数据,那么就需要权限来获取应用提供的功能,应用以静态的方式声明他们需要的权限,然后 Android 系统提示用户同意。
3.什么是应用签名?
Android 系统要求每一个应用程序必须经过数据签名才能安装到系统中,也就是说,如果一个 Android 程序没有经过数字签名,就不能安装到系统中。这个数字签名并不需要西像 HTTPS 协议一样有权威的机构进行发布,完全可以有应用程序的作者完成,用于建立这个应用的作者和应用程序之间的信任关系。
在实际的开发中,应用可以直接在模拟器上或者真机上运行是因为 ADT 会自动的使用 debug 密钥为应用程序签名。
应用签名的好处
- 在应用升级的时候,签名证书相同,包名相同的应用才被认为是同一个应用,因此也可以避免一些伪装的程序安装到系统上。
- 将应用程序模块化,Android 系统可以允许同一个证书签名的多个应用程序在一个进程里运行,系统实际把他们作为一个单个的应用程序,此时就可以把应用程序以模块的方式进行部署,而用户可以独立的升级其中的一个模块。
- 代码或者数据共享:基于签名的权限机制,一个应用程序就可以为另一个以相同证书签名的应用程序公开自己的功能。以同一个证书对多个应用程序进行签名,利用基于签名的权限检查,就可以在应用程序间以安全的方式共享代码和数据了。
4.shareUserId 属性
在安装的时候,Android 为每个软件包提供了唯一的 Linux 用户 ID .在不同的设备上,相同的软件包可能有不同的 UID ,重要的是每个软件包在指定设备上的 UID 是唯一的。
另外,我们可以在每个应用程序的 AndroidManifest.xml 的 manifest 标记中使用 sharedUserId 属性,为它们分配相同的用户 ID。这样做以后,出于安全目的,两个软件包将被视为同一个应用,具有相同的用户 ID 和文件权限。但是为保持安全性,只有两个签署了相同签名(并且请求相同的 sharedUserId)的应用才被分配同一用户 ID。
应用存储的任何数据都会被分配该应用的用户 ID,并且其他软件包通常无法访问这些数据
使用 shareUserId 示例
首先在 两个不同的应用里要指定同样的 shareUserId
进程 A1
2
3
4<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="apiratehat.androidsamplecode"
android:sharedUserId="share.ContentProvider" > <!--设置sharedUserId, 名字中至少要有一个 . 符号-->
进程 B1
2
3
4<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="apiratehat.androidsamplecode2"
android:sharedUserId="share.ContentProvider" > <!--设置sharedUserId,至少要有一个 . 符号-->
然后再进程 A 中指定一个 注册一个 ContentProvider1
2
3<provider
android:authorities="apiratehat.androidsamplecode"
android:name=".permission.ShareIdContentProvider"/>
接着就可以在 进程 B 中获取到 进程 A 的 Context 对象1
2
3
4
5
6
7
8Context context = null;
try {
// 指定包名为 进程 A 的包名
context = createPackageContext("apiratehat.androidsamplecode", Context.CONTEXT_IGNORE_SECURITY|CONTEXT_INCLUDE_CODE);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
在参数中有两个 flag
Context.CONTEXT_IGNORE_SECURITY 表示忽略所有的权限问题,必须指定
CONTEXT_INCLUDE_CODE 表示可以调用 Context 的代码
拿到对应的 Context 就可以执行对应的方法 比如访问 ContentProvider 或者通过反射获取在进程 A 环境下的其他类,达到 进程 B 和 进程 A 共享代码和数据的场景(常用在组件化开发上)。1
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
需要注意的是,使用 shareUserId 要求里两个软件包拥有相同的 签名,为了测试的方便,可以指定 DEBUG 时候的签名,网上有很多例子,这里就不再赘述。
4.权限
在默认的情况下 Android 应用默认未关联权限,这意味着在不指定权限的情况下,一个应用无法执行对用户体验或者设备任何数据产生不利影响的任何操作。如果要要利用受保护的设备功能,必须在应用清单中包含一个或者多个1
2
3```
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
权限的分类
- 危险权限,涉及到用户隐私信息的权限都是属于危险的权限,比如读写联系人信息,读写外部 SD 卡存储的信息就是正常权限。如果应用声明其需要正常权限,系统会自动向应用授予该权限。
- 正常权限,访问对用户隐私或其他应用操作风险很小的区域,比如网络,蓝牙等,如果应用声明其需要正常权限,系统会自动向应用授予该权限。
在 Android 5.0 之前(API 22) 之前,系统会在用户安装应用时要求用户授予权限。如果将新权限添加到更新的应用版本,系统会在用户更新应用时要求授予该权限。用户一旦安装应用,他们撤销权限的唯一方式是卸载应用。
上面机制有个明显的缺点的就是用户需要授予授予所有的权限才能用使用,不能根据自己的需要选择性的授予某些权限,使用某些功能,因此在 Android 6.0 (API 23) 的时候,引入了运行时权限检查。
在 Android 6.0 之后 (API 23 )之后,对于危险的权限,应用在运行时向用户请求权限。用户可随时调用权限,因此应用在每次运行时均需检查自身是否具备所需的权限。同时用户也就可以 在应用权限管理中更改应用的权限。
权限组
所有危险的 Android 系统权限都属于权限组。如果设备运行的是 Android 6.0(API 级别 23),并且应用的 targetSdkVersion 是 23 或更高版本,则当用户请求危险权限时系统会发生以下行为:
- 如果应用请求其清单中列出的危险权限,而应用目前在权限组中没有任何权限,则系统会向用户显示一个对话框,描述应用要访问的权限组。对话框不描述该组内的具体权限。例如,如果应用请求 READ_CONTACTS 权限,系统对话框只说明该应用需要访问设备的联系信息。如果用户批准,系统将向应用授予其请求的权限。
- 如果应用请求其清单中列出的危险权限,而应用在同一权限组中已有另一项危险权限,则系统会立即授予该权限,而无需与用户进行任何交互。例如,如果某应用已经请求并且被授予了 READ_CONTACTS 权限,然后它又请求 WRITE_CONTACTS,系统将立即授予该权限。
如果设备运行的是 Android 5.1(API 级别 22)或更低版本,并且应用的 targetSdkVersion 是 22 或更低版本,则系统会在安装时要求用户授予权限。再次强调,系统只告诉用户应用需要的权限组,而不告知具体权限。
在实际开发中,对应的权限管理 和 targetSdkVersion 还有 运行的设备有关。
系统通过 targetSdkVersion 来保证 Android 的向前兼容性, 在 Android 5.0 之后的设备上,系统会判断你的 targetSdkVersion 是否小于 22 ,如果小于的话,那就按照 22 之前的 api 方法,如果大于等于 22 ,那么就按照之后的 api 方法来走,保证了程序运行的一致性。也就是向前兼容性.
在测试,如果访问危险权限
targetSdkVersion == 22
- 在 Android 6.0 的手机上(获取联系人数据),成功返回数据
- 在 Android 5.0 的手机上(获取联系人数据),成功返回数据
targetSdkVersion == 27
- 在 Android 6.0 的手机上(获取联系人数据),抛出异常,因为 6.0 需要设置运行时权限管理
- 在 Android 5.0 的手机上(获取联系人数据),成功返回数据
二.运行时权限管理
运行时权限管理一般涉及的方法有
- Build.VERSION.SDK_INT 设备 SDK 运行的版本,如果 设备是 Android 6.0 就返回 23
- Build.VERSION_CODES.xx Android 对应的版本的 API ,比如 Build.VERSION_CODES.M 就是 API 23 ,后面的字母是 Android 版本对应的 代号
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.M 就表示判断当前版本是否是 Android 6.0 及以上。
- ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED 用于检查对应的的权限是否授权
- requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, 2); 请求对应的权限,后加 对应的请求号,每个权限对应一个请求号。
- onRequestPermissionsResult 请求权限后的结果回调
- shouldShowRequestPermissionRationale(xx) 是否有弹窗提示权限请求
在进行权限请求的时候,如果是第一次请求,那么就会有类似如下的场景
当我们拒绝的时候,如果再次申请权限,那么就会有多一个 “不再询问” 标识
如果没有点击不再询问,那么重新申请的时候就会弹出提示的窗口,如果点击了不再询问,那么以后都不会弹出申请框。因此为了良好的用户的体验,我们需要在这里设置一个提示,表示用户可以去设置中打开权限。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76public void click(View v) {
//不需要申请权限的时候直接使用 5.0 版
// startActivityForResult(new Intent(
// Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI), 0);
//需要申请的权限的时候先判断权限
//只有 6.0 以上 ,才需要运行时权限, M 是版本代号 API 为 23
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//检查权限 是否授权
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
//请求授权
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, 2);
} else {
//授权了就 进行请求
startActivityForResult(new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI), 0);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// //没有点击 不再询问返回 true ,点击不再询问后就返回 false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !shouldShowRequestPermissionRationale(permissions[0])){
showMissingPermissionDialog();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
//
}
}
//// 显示缺失权限提示
private void showMissingPermissionDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(PermissionActivity.this);
builder.setTitle("帮助");
builder.setMessage("当前应用缺少必要的权限,请打开 “设置” -> “权限” 打开所需权限");
// 拒绝, 退出应用
builder.setNegativeButton("退出", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.setPositiveButton("设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startAppSettings();
}
});
builder.show();
}
// 启动应用的设置
private void startAppSettings() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
三.自定义权限
当要自定义自己的权限的时候,必须先使用一个或多个
比如想要控制谁可以开始自己应用中一个 Activity 的应用可如下所示声明此操作的权限:1
2
3
4
5
6
7
8
9<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp" >
<permission android:name="com.example.myapp.permission.DEADLY_ACTIVITY"
android:label="@string/permlab_deadlyActivity"
android:description="@string/permdesc_deadlyActivity"
android:permissionGroup="android.permission-group.COST_MONEY"
android:protectionLevel="dangerous" />
...
</manifest>
注:系统不允许多个apk 使用同一名称声明权限,除非所有 apk 都使用同一签名 。如果已经有一个 apk 声明权限,则系统不允许用户安装具有相同权限名称的其他软件,除非这些软件包使用与第一个软件包相同的签名。为避免命名冲突,建议对自定义权限使用相反域名样式命名,例如 com.example.myapp.ENGAGE_HYPERSPACE。
其中几个属性介绍如下
protectionLevel
保护级别一共有四种
- normal 如果应用声明了此权限,也不会提示安装应用的用户授权。比如网络请求等都是属于这个级别
- dangerous 这种级别的权限风险更高,拥有此权限可能会访问用户私人数据或者控制设备,给用户带来负面影响,这种类型的权限一般不会默认授权
,比如读写 SD ,联系人信息就属于这种。 - signature:这种权限级别,只有当发请求的应用和接收此请求的应用使用同一签名文件,并且声明了该权限才会授权,并且是默认授权,不会提示用户授权
- signatureOrSystem:这种权限应该尽量避免使用,偏向系统级
android:permissionGroup
属性是可选属性,只是用于帮助系统向用户显示权限。大多数情况下,您要将此设为标准系统组(列在 android.Manifest.permission_group 中),但您也可以自己定义一个组。建议使用现有的组,因为这样可简化向用户显示的权限 UI。
android:description/android:label
需要为权限提供标签和描述。这些是用户在查看权限列表或单一权限详细信息时可以看到的字符串资源。
四.给四大组件设置权限
通过 AndroidManifest.xml 添加权限,可以限制访问系统或应用的组件。只要在所需的组件上包含 android:permission 属性,就可以达到限制只有拥有的权限的进程才能访问,没有权限的进程就不能访问。
Activity
在 1
2
3
4
5
6
7
8
9
10
11
12<permission android:name="apiratehat.androidsamplecode.permission.DEADLY_ACTIVITY"
android:label="@string/apply"
android:description="@string/apply"
android:permissionGroup="android.permission-group.COST_MONEY"
android:protectionLevel="dangerous" />
<activity
android:permission="apiratehat.androidsamplecode.permission.DEADLY_ACTIVITY"
android:name=".permission.PermissionActivity">
</activity>
Service
应用于
BroadcastReceiver /发送
接收
BroadcastReceiver 权限(应用于
发送
在发送的时候,可以在调用 Context.sendBroadcast() 时提供权限来限制允许哪些 BroadcastReceiver 对象接收广播。
ContentProvider
应用于
五.Uri 权限
ContentProvider 可能需要通过读取和写入权限保护自己的某段数据或者部分数据, per-URI 权限机制:比如在启动 Activity 或返回结果给 Activity 时,调用方可以设置 Intent.FLAG_GRANT_READ_URI_PERMISSION 和/或 Intent.FLAG_GRANT_WRITE_URI_PERMISSION。这可以让 接收 的Activity 有权限去访问 intent 中的特定数据 URI,而不管它是否具有访问 intent 对应的内容提供程序中数据的任何权限,换句话说就是 Activity A 有访问 APP C ContentProvider 的权限,但是 Activity B 没有,Activity A 可以通过 设置 这个临时访问的标志启动 Activity B ,这个时候 Activity B 就可以拿到 Intent 临时访问 APP C 的 ContentProvider 。
官方建议内容提供程序实施此功能,并且通过 android:grantUriPermissions 属性或
在 Context.grantUriPermission()、Context.revokeUriPermission() 和 Context.checkUriPermission() 方法中可以找到更多信息。