Android M PackageManager应用程序权限管理源码剖析及runtime permission实战
上一篇文章我们介绍了android的包管理器PackageManager对于应用程序apk的安装流程,今天我们主要研究应用程序权限的管理部分。
Android应用权限授予部分主要分为两部分,第一部分是在PKMS启动之后,且扫描完所有的app后,会对应用程序分配linux用户组ID,即授予他们所申请的资源访问权限。
第一部分主要是对install等权限进行无条件授予,而许多核心app的默认权限则还没有授予,对于用户体验来说相对差些,因此,许多必要的默认权限(runtime permission)则会在PKMS.systemReady()方法中去进一步完成。
基本概念
在介绍这两部分内容之前,我们先介绍如下概念:
用户id与组id
/*UID和GID*/
如果你是一个Linux的用户,那么一定会有如下的体验。当你的Linux的系统里面,存在多个用户的时候,你在登录了用户A之后,是没有权限去修改用户B的文件内容的。
再举个极端的例子,用普通用户的权限是没有办法进行Root用户所特有的操作的。
UID:
Linux系统用于区别不同的用户,使用不同的用户名是一种方法。但是用户名只是一种让人方便读的字符串,对机器来讲是没有意义的。
为了方便机器的读取,Linux采用了一个32位的整数记录和区分不同的用户。这个用来区分不同用户的数字被称为UserID,简称UID。
root的UID为0,普通的UID一般是从500开始。在Android上,一个用户 UID标示一个应用程序,相当于linux中的一个用户。应用程序在安装时被分配用户UID,应用程序在设备上的存续期间内,用户UID保持不变。对于普通的应用程序,GID即等于UID。
GID:
除了用户的概念,Linux的系统还有用户组的概念。同一个用户组的用户之间具有相似的特征。
假如我们把某一个用户加入到root组,那么这个用户就可以浏览root用户组目录的文件。如果root用户把某个文件的读写执行权限开放,root用户组的所有用户都可以修改该文件。
Permission概念
Package的权限信息主要通过在AndroidManifest.xml中通过一些标签来指定。
如 <permission> 标签, <permission-group> 标签, <permission-tree> 等标签。如果package需要申请使用某个权限,那么需要使用<use-permission> 标签来指定,系统的权限定义在frameworks/base/core/res/AndroidManifest.xml(framework-res.apk)当中这些权限会由PKMS启动进行扫描的时候添加到对应的数据结构里。
比如要想使用网络功能:
<uses-permissionandroid:name="android.permission.INTERNET" />
这个权限在APK安装扫描过程中会保存在PackageParser.Package.requestedPermissions中,其在platform.xml中对应的item如下:
<permission name="android.permission.INTERNET" >
<group gid="inet" />
</permission>
这里定义了上层android.permission. INTERNET权限对应的Linux用户组gid为inet。并且在解析xml文件的时间也为每个permission标签创建一个BasePermission对象,该对象中包含一个gids数组,用来保存与该权限对应的Linux用户组。
mGlobalGids数组的值是解析platform.xml文件得到的。
· 一个权限主要包含三个方面的信息:权限的名称,属于的权限组和保护级别。一个权限组是指把权限按照功能分成的不同的集合。每一个权限组包含若干具体权限,例如在storage权限组中包含android.permission.READ_EXTERNAL_STORAGE,android.permission.WRITE_EXTERNAL_STORAGE两个和存储访问相关的权限。
Android权限等级划分
Android权限等级划分为normal,dangerous,signature,signatureOrSystem,system,development,不同的保护级别代表了程序要使用此权限时的认证方式。
normal
在androidmanifest.xml中声明相应的权限,在安装应用时,会默认获得许可。并且用户不能修改权限许可。(只需要在AndroidManifest.xml中简单声明这些权限就好,安装时就授权。不需要每次使用时都检查权限,而且用户不能取消以上授权。)
dangerous
权限在安装运行时需要用户确认才可以使用,具体申请使用规则会在后文介绍
signature
需要与定义该权限的apk签名一致才能赋予权限
signatureOrSystem
需要与定义该权限的apk签名一致,或者是系统级应用(放置在/system/app目录下)才能使用该权限。
对于上述四种权限级别我们举个例子:
比如百度地图apk的AndroidManifest.xml里面声明了一个权限,
(1) 权限定义为Dangerous,那么任何其他应用都可以使用。
(2) 权限定义为Signature,那么只有使用同样私钥签名的apk,例如百度网盘,可以使用这个权限。
(3) 权限定义为SignatureOrSystem,那么使用同样私钥签名的apk,例如百度网盘,可以使用这个权限。
另外在/system/priv-app下的特权应用才可以使用这个权限,关于这一点可以在代码中看出来:
接下来我们具体了解下android的权限管理。
PKMS权限管理
PKMS在扫描阶段会解析出每个apk的信息,这些信息会存在一个以pkgName为key,以pkg为value的ArrayMap<String, PackageParser.Package> mPackages的成员对象中,而pkg对象有一个mExtras成员则存储的是PackageSettings对象,它用来描述这个pkg的安装信息,而PackageSetting对象继承自SettingsBase对象,这个对象包含有一个PermissionsState对象,PermissionsState对象有个成员ArrayMap<String,PermissionData> mPermissions,可以看到它里面存储的是PermissionData对象,而PermissionData有如下两个成员:
private final BasePermission mPerm; // 描述一个基本的权限
private SparseArray<PermissionState>mUserStates = new SparseArray<>(); // 存储该权限的的状态
public static final class PermissionState {
private final String mName;
private boolean mGranted;
private int mFlags;
}
权限相关类图如下:
有了这个关系,我们接下来就开始我们的第一部分。
updatePermissionsLPw,会进一步调用grantPermissionsLPw,其主要流程如下:
最终通过PermissionData的grant方法将PermissionState的状态修改。
第一部分到此结束。
默认runtime权限授予
前面提到第一部分主要是对install等权限进行无条件授予,而许多核心app的默认权限则还没有授予,对于用户体验来说相对差些。
因此,许多必要的默认权限(runtime permission)则会在PKMS.systemReady()方法中去进一步完成,我们接下来看这一部分。
这段代码逻辑在PKMS.systemReady中,遍历所有的user,对于没有进行default权限设置的则调用grantDefaultPermissions,默认的权限赋予工作是由DefaultPermissionGrantPolicy去完成的。
final DefaultPermissionGrantPolicy mDefaultPermissionPolicy =
new DefaultPermissionGrantPolicy(this);
权限检查
Androidframework中提供了一些接口用来对外来的访问(包括自己)进行权限检查。这些接口主要通过ContextWrapper提供,具体实现在ContextImpl中。
如果package接受到外来访问者的操作请求,那么可以调用这些接口进行权限检查。一般情况下可以把这些接口的检查接口分为两种,一种是返回错误,另一种是抛出异常。
返回为错误类型的API如下:
方法 | 说明 |
public int checkPermission(String permission, int pid, int uid) | 检查某个uid和 pid 是否有permission权限 |
public int checkCallingPermission(String permission) | 检查调用者是否有 permission 权限,如果调用者是自己那么返回 PackageManager.PERMISSION_DENIED |
public int checkCallingOrSelfPermission(String permission) | 检查自己或者其它调用者是否有 permission 权限 |
|
|
checkPermission 实现分析:
1. 如果传入的permission名称为null,那么返回PackageManager.PERMISSION_DENIED 。
2. 判断调用者uid是否符合要求。
1 )如果uid为0,说明是root权限的进程,对权限不作控制。
2 )如果uid为system server进程的uid 说明是 system server,对权限不作控制。
3 )如果是ActivityManager进程本身,对权限不作控制。
4 )如果调用者 uid 与参数传入的 req uid不一致,那么返回 PackageManager.PERMISSION_DENIED 。
3. 如果通过2的检查后,再调用PackageManagerService.checkUidPermission,判断这个uid是否拥有相应的权限,分析如下:
1 )首先它通过调用getUserIdLP,去PackageManagerService.Setting.mUserIds 数组中,根据uid查找uid(也就是package)的权限列表。一旦找到,就表示有相应的权限。
2 )如果没有找到,那么再去PackageManagerService.mSystemPermissions中找。这些信息是启动时,从/system/etc/permissions/platform.xml中读取的。这里记录了一些系统级的应用的uid对应的permission。
3 )最后返回结果。
抛出异常的API如下:
方法 | 说明 |
public void enforcePermission(String permission, int pid, int uid, String message) | SecurityException |
public void enforceCallingPermission(String permission, String message) | SecurityException |
public void enforceCallingOrSelfPermission(String permission, String message) | SecurityException |
RuntimePermission
什么是Runtime Permission?
安卓是一个权限分离的操作系统,在该系统中,每个应用程序都运行在一个不同的系统身份中(用户身份和组标识)。该系统的部分也被分离成不同的身份。从而使应用程序从系统中分离。
通过“权限”机制,执行具体的操作如限制一个特定的进程可以提供额外的安全特性以及授予每个URI权限去临时访问特定的数据块。
简而言之,应用运行时,所申请的一些运行权限,比如:美图相机它要用到摄像头,这个摄像头使用是要有权限的,用户允许则可以使用,不允许就无法使用。类似的还有地图类的应用申请使用GPS,语音类的应用申请使用麦克风(Audio)等。
Android M Runtime Permission的不同
对于棉花糖M以前的系统版本,app的权限是在安装时被授予的。Android 6.0以后,app将不会在安装时被授予权限,取而代之的是,app需要在运行时一个个的被询问授予权限,如下所示:
在box或者平板中的权限申请图:
在手机设备中的权限申请图:
Android6.0之后的版本,应用要申请危险级别的权限时,首先需要在Manifest中申请,然后需要在app内部中,如果对应的操作需要相应的权限(比如访问storage),则需要先向系统申请权限,然后才能使用相应的操作,如果不处理,则会导致应用崩溃。
AndroidM+权限申请实战
关于Android M权限的申请使用,这里以使用联系人的权限为例,新建app从权限的介绍,权限的声明及使用详细的进行分解,以下内容仅针对Android6.0进行探讨,下文中不做特别说明。
SDK的配置细节
1.开发工具建议使用android studio1.3以及更新的版本;
2.SDK更新到API 23版本;
3.新建工程时:
注意选择:“Phone and Tablet”;
Minimum SDK 选择: “API 23:Android 6.0(Marshmallow)
4. 对于已经建立的工程要对build.gradle(Module:app)进行更新设置:
a) compileSdkVersion 设置到23;
b) targetSdkVersion 设置到23;
c) compileSdkVersion设置到23;
权限申请的一般流程如下:
权限请求实例
1.首先需要在manifest中申请需要使用的权限<uses-permission>标签
2.其次在app代码中通过Context.checkSelfPermission(permission_name)API去检测所要申请的权限:
3.在回调中处理用户权限申请结果
最后豁免的权限会通过Settings持久化到runtime-permissions.xml中去
对于不在提醒的选项被勾选后,下次请求权限,将不再弹出权限请求的对话框,而是直接回调onRequestPermissionsResult方法,且回调结果为用户上一次的选择。为了应对这种情况,系统提供了另一个API-shouldShowRequestPermissionRationale,这个函数的作用是帮助开发者找到需要向用户额外解释权限的情况,这个函数的返回情况如下:
1. 应用安装后第一次访问,直接返回false;
2. 第一次请求权限时,用户拒绝了,下一次shouldShowRequestPermissionRationale()返回 true,这时候可以显示一些为什么需要这个权限的说明;
3. 第二次请求权限时,用户拒绝了,并选择了“不再提醒”的选项时:shouldShowRequestPermissionRationale()返回 false;
4. 设备的系统设置中禁止当前应用获取这个权限的授权,shouldShowRequestPermissionRationale()将会返回false;
与权限相关的PM命令
1. 让app拥有权限的安装
adb install -g <path to apk>
例如:adb install –g xxxx.apk
很明显用这个命令安装apk在安装时就直接赋予了app所有要申请的权限,为安全着想仅建议开发调试使用。
2. 允许/撤销权限
允许某一权限:
adb shell pm grant <package_name><permission_name>
运行该命令后允许package_name程序的permission_name权限,如:
adb shell pm grant pkgNameandroid.permission.READ_EXTERNAL_STORAGE
注意:该命令仅开启命令所要求开启的权限,对权限组的权限并不因为是权限组的原因会连带开起,即,当使用命令开启了Contacts的写权限时,并没有连带将Contacts权限组的其他的权限也开启了,这时候当你要调用读权限时,app由于没有Contacts的读权限而crash。
撤销某一权限:
adb shell pm revoke <package_name> <permission_name>
由之前权限分析可知,我们的grant方法最终是将之前的mUserStates中的permissionState的mGranted状态置为true,而revoke则是将其值设为false。