Android 权限机制,你真的了解吗?

9,594 阅读21分钟
原文链接: mp.weixin.qq.com

一、Android的权限机制

Android是目前最流行的智能手机软件平台之一,在智能移动终端如火如荼发展的同时,其安全态势也日益严峻。有调查表明,恶意软件的数量在持续的上升,Google在Android安全机制上面也做了很多工作,并且一直在持续的更新,其Android的安全模型由3个部分组成:Linux安全机制、Android本地库及运行环境安全与Android特有的安全机制,如下图:查看图片  

本文只涉及到其中的权限机制介绍,其他的部分如果有感兴趣的,我们可以后续一起探讨。

Android的权限管理遵循的是“最小特权原则”,即所有的Android应用程序都被赋予了最小权限。一个Android应用程序如果没有声明任何权限,就没有任何特权。因此,应用程序如果想访问其他文件、数据和资源就必须在AndroidManifest.xml文件中进行声明,以所声明的权限去访问这些资源。否则,如果缺少必要的权限,由于沙箱的保护,这些应用程序将不能够正常提供所期望的功能与服务。

所有应用程序对权限的申请和声明都被强制标识于AndroidManifest.xml文件之中,通过,,等标签指定。如果需要申请某个权限,可以通过指定。应用程序申请的权限在安装时提示给用户,用户可以根据自身需求和隐私保护决定是否允许对该应用程序授权。

 

二、权限基本知识

2.1 权限的类别

由于基于Linux内核,Android系统中的权限分为以下3类。

(1)Android手机所有者权限

这个和厂商相关,可以理解为系统权限。

(2)Android ROOT权限

类似于Linux,这是Android系统中的最高权限。如果拥有该权限,就可以对Android系统中的任何文件、数据、资源进行任意操作。所谓“越狱”,就是令用户获得最高的ROOT权限。

(3)Android应用程序权限

该权限在AndroidManifest文件中由程序开发者声明,在程序安装时由用户授权,共有下述4类不同的权限保护级别(Protection Level)。


2.2 Protection level

我们经常在AndroidManifest中使用权限,如果我们想让应用程序可以发短信,那么应该这样写:


其权限的定义是在frameworks/base/core/res/AndroidManifest.xml中,如下:


.permission.SEND_SMS" 

android:permissionGroup="android.permission-group.COST_MONEY" 

android:protectionLevel="dangerous" 

android:label="@string/permlab_sendSms" 

android:description="@string/permdesc_sendSms" /> 

 

这个XML可以认为是系统APK使用的AndroidManifest.xml,该APK使用系统的私钥进行签名。

下面分别简单介绍下各个标签的含义:

android:name:权限的名字,uses-permisson使用的。

android:permissionGroup:权限的分类,在提示用户安装时会把某些功能差不多的权限放到一类。

android:protectionLeve:分为NormalDangerousSignatureSignatureOrSystem。

android:label:提示给用户的权限名。

android:description:提示给用户的权限描述。

其中android:protectionLevel各个属性说明如下:

(1)Normal

风险较低的权限,任何应用都可以申请,在安装应用时,不会直接提示给用户,点击全部才会展示。

(2)Dangerous

风险较高的权限,任何应用都可以申请,安装时需要用户确认才能使用

(3)Signature

仅当申请该权限的应用程序与声明该权限的程序使用相同的签名时才赋予该权限

(4)SignatureOrSystem

仅当申请该权限的应用程序位于相同的Android系统镜像中或申请该权限的应用程序与声明该权限的程序使用相同的签名时才赋予该权限

可以这样理解:

1)和该APK(定义了这个权限的APK)用相同的私钥签名的应用。

2)在/system/app目录下的应用。

 

2.3 进程的权限表现

Android是一个多进程系统,在这个系统中,应用程序会在自己的进程中运行,系统和应用之间的安全性是通过Linux进程级别来强制实现的,会给应用程序分配userID和GroupID。

比如我们查看qqdownload这个进程,adb shell后查看下其进程id(红色部分):查看图片根据id,执行下查看状态,如下:查看图片 

我们关注如下三行:

Uid:10301   10301   10301   10301

Gid:10301   10301   10301   10301

Groups:1006 1013 1015 1028 3001 3002 3003 9997 50301

这里我们便看到了系统进程的权限配置信息,这里的数字具体代表意义,可以在Android

\system\core\include\private\android_filesystem_config.h里面看到,其部分内容如下:

#define AID_ROOT             0  /* traditional unix root user */

#define AID_SYSTEM        1000  /* system server */

#define AID_RADIO         1001  /* telephony subsystem, RIL */

#define AID_BLUETOOTH     1002  /* bluetooth subsystem */

#define AID_GRAPHICS      1003  /* graphics devices */

#define AID_INPUT         1004  /* input devices */

#define AID_AUDIO         1005  /* audio devices */

#define AID_CAMERA        1006  /* camera devices */

#define AID_LOG           1007  /* log devices */

#define AID_COMPASS       1008  /* compass device */

#define AID_MOUNT         1009  /* mountd socket */

#define AID_WIFI          1010  /* wifi subsystem */

#define AID_ADB           1011  /* android debug bridge (adbd) */

#define AID_INSTALL       1012  /* group for installing packages */

#define AID_MEDIA         1013  /* mediaserver process */

...

其中qqdownload对应的Groups描述如下:

1006 camera devices

1013 mediaserver process

1015 external storage write access

1028 external storage read access

3001 bluetooth: create any socket

3002 bluetooth: create sco,rfcomm or l2cap sockets

3003 can create AF_INET and AF_INET6 sockets

9997 shared between all apps in the same profile

50301 start of gids for apps in each user to share

不同的手机这个信息显示的不一样,这个是vivo手机的信息,在华为手机上只有3001 3002 3003 9997 50689。

我们拿其中一个写SD卡的权限来简单说明一下:

写SD卡权限是

  

我们看一个重要的文件,frameworks\base\data\etc\Platform.xml里的内容:

  

  

  

在看下图:查看图片

 

对于申请了WRITE_EXTERNAL_STORAGE特权的应用,该应用的进程的gids就包含了sdcard_rw,就可以对sd卡中的文件进行操作了。

再看Android_filesystem_config.h的源码:

#define AID_SDCARD_RW 1015 /* external storage write access */ 

 

我们看到qqdownload进程的Groups里面有gid为1015,1015就是对应sdcard_rw。

 

以上介绍了进程的权限表现,实际工作中,我们可能不需要关注这些。

 

2.4 Android 系统对应用程序权限申请的处理方式分析

Android系统对应用程序授权申请的处理流程:

(1)进入处理应用程序授权申请的入口函数;

(2)系统从被安装应用程序的AndroidManifest.xml文件中获取该应用正常运行需申请的权限列表;

(3)显示对话框,请求用户确认是否满足这些权限需求;

若同意,则应用程序正常安装,并被赋予相应的权限;若否定,则应用程序不被安装。系统仅提供给用户选择“是”或者“否”的权利,没有选择其中某些权限进行授权的权利。

具体如下图:

查看图片

 

2.5 Android原生权限管理:AppOps

2.5.1 AppOps 简介

AppOps全称是Application Operations,类似我们平时常说的应用程序的操作权限管理,AppOps是Google原生Android包含的功能,但是Google在每次版本更新时都会隐藏掉AppOps的入口,Google高管Hiroshi Lockheimer的原话:“App ops发布的时机不太对头,我们需要全面解决问题,而不是单独地发布Appops”。

在今年的GoogleIO大会上,Google透露AndroidM(Android 6.0)会加入Application Permission Manage的功能,该功能应该就是基于AppOps实现的。

注意:AppOps虽然涵盖了App的权限管理,但是Google原生的设计并不仅仅是对“权限”的管理,而是对App的“动作”的管理。我们平时讲的权限管理多是针对具体的权限(App开发者在Manifest里申请的权限),而AppOps所管理的是所有可能涉及用户隐私和安全的操作,包括access notification,keep weak lock,activate vpn,display toast等等,有些操作是不需要Manifest里申请权限的。


2.5.2 功能效果

Setting UI:

AppOps的权限设置是在系统的Settings App里,Settings->Security->AppOps。

点击某一app,可以查看该app的权限管理详情,也可以设置显示。

 

使用效果:

AppOps默认给用户提供了两个设置选项:

允许该项权限/禁止该项权限

 

而其实代码逻辑里,有三种可选项:

允许/禁止/提示

用户选择“提示”选项,则该app在执行这一操作时,系统会给用户相应的提示,待用户选择后app继续执行。

    

2.5.3 AppOps总体概览

核心服务:AppOpsService

系统服务,系统启动时该服务会启动运行。

参考以下ActivityManagerService.java,ActivityManagerService启动过程中:

查看图片

配置文件:appops.xml、appops_policy.xml

Appops.xml位于/data/system/目录下,存储各个app的权限设置和操作信息。

Appops_policy.xml位于/system/etc/目录下,该文件只在appops strict mode enable时才会存在和使用。

 

API接口:AppOpsManager

AppOpsService实现了大部分的核心功能逻辑,但它不能被其他模块直接调用访问,而是通过AppOpsManager提供访问接口。

 

UI层:AppOpsSummary,AppOpsCategory等

上传UI显示以及基本逻辑处理。

 

2.5.4 结构图

AppOps整体的工作框架基本如下:

 

 查看图片
 

Setting UI通过AppOpsManager与AppOpsService交互,给用户提供入口管理各个app的操作。

AppOpsService具体处理用户的各项设置,用户的设置项存储在/data/system/appops.xml文件中。

AppOpsService也会被注入到各个相关的系统服务中,进行权限操作的检验。

各个权限操作对应的系统服务(比如定位相关的Location Service,Audio相关的Audio Service等)中注入AppOpsService的判断。如果用户做了相应的设置,那么这些系统服务就要做出相应的处理。

(比如,LocationManagerSerivce的定位相关接口在实现时,会有判断调用该接口的app是否被用户设置成禁止该操作,如果有该设置,就不会继续进行定位。)

 

2.5.5 相关API接口

尽管在Android SDK里能够看到部分AppOps的API接口,但是Google对此解释的很清楚:

This API is not generally intended for third party application developers; most features are only available to system applications。Obtain an instance of it throughContext.getSystemService withContext.APP_OPS_SERVICE。

即是说,这些API不是让第三方app使用的,而是供系统应用调用的。

使用Android SDK开发应用,如果要调用这些API的话,也会编译不通过。

但是想使用的话,可以尝试把Android源码里AppOpsManager.java打包一下,把jar包导入自己的工程,就可以使用了。

 

部分重要的API接口如下:

 

int

checkOp(String  op,int uid,String packageName)

Op对应一个权限操作,该接口来检测应用是否具有该项操作权限。

 

int

noteOp(String op, int uid,String packageName)

和checkOp基本相同,但是在检验后会做记录。

 

int

checkOpNoThrow(String op,int uid,String packageName)

和checkOp类似,但是权限错误,不会抛出SecurityException,而是返回AppOpsManager。MODE_ERRORED。

 

int

noteOpNoThrow(String op,int uid,String packageName)

类似noteOp,但不会抛出SecurityException。

 

void setMode(int code,int uid,String packageName,int mode)

这个是我们最需要的方法,改变app的权限设置,但偏偏被google隐藏了。

code代表具体的操作权限,mode代表要更改成的类型(允许/禁止/提示)

 

正常情况下(如果OEM厂商没有做特殊处理),把AppOpsManager.java打包,引入jar包到工程内,是可以使用上述API接口的,

也即是可以自行设计UI,提供入口来改变app权限。 

具体权限对应的code,可以查看AppOpsManager.java源码里的描述。

 

三、权限变化趋势

Android M之前,应用的权限请求是在安装时提示,确认后权限就会拥有。

但Android M出来后,将这个权限在运行时做了进一步的检查,用户随时可拒绝权限。

● 从平台角度看:Android权限集不断扩展,但不是以提供更细粒度的权限为目标,而是为访问新的硬件功能提供安全保障。特别地,Dangerous权限集的数量也在不断增多;

● 从第三方应用和预装应用角度看:大量应用并未遵守最小特权原则,而是存在大量过多申请权限的情形。值得注意的是:许多预装应用使用大量高级别的权限,带来很大的安全隐患。

用户只有通过不断学习,充分理解新加入的权限说明,才能在安装软件时从Android权限警告中获取足够的信息,从而做出正确的决定。鉴于Android系统每隔数月就有较大的版权更新,并引入较多新的权限,这为用户提出了很高要求。


四、Android M变化以及带来的影响

从Android6.0(API LEVEL23)开始,用户对应用权限进行授权是发生在应用运行时,而不是在安装时。这样可以让用户在安装时节省时间,而且可以更方便的控制应用的权限(至少权限管理不需要ROOT了)。用户可以按照对应用的需求来控制应用的权限,比如百度地图的联系人权限。同时用户也可以在应用程序设置中撤销对应用的权限授权。

Android系统中的权限被划分为两类:普通权限和敏感权限(更多普通权限、敏感权限及权限组信息:

普通权限不会涉及到用户隐私,如果应用在manifest文件中直接声明了普通权限,系统会自动授予权限给应用。比如:网络INTERNET、蓝牙BLUETOOTH、震动VIBRATE等权限。

敏感权限则要获取到一些用户私密的信息。如果你的应用需要获取敏感权限,首先需要获取用户的授权。比如:相机CAMERA、联系人CONTACTS、存储设备STORAGE。

https://developer.android.com/reference/android/Manifest.permission.html

详情见上面这个链接,每个权限里面可能会有Protection level,标记着是dangerous还是normal

Android的各个版本中,不论是普通权限还是敏感权限,都需要在manifest文件中声明,例如权限声明。然而,在不同版本的操作系统或不同的target SDK level中的结果是不同的。

如果设备运行Android5.1或者更低版本的操作系统,或者你的目标SDK版本号小于或等于22,当你在manifest文件中请求了一些权限,用户必须在安装过程时授予全部权限,否则应用不能正常安装。

如果设备运行在Android6.0或者更高版本,并且目标SDK版本号大于或等于23,应用程序必须要在manifest文件中声明需要的权限,当程序运行时,它必须要向用户请求授权每个所需的敏感权限。用户可以允许或拒绝每个权限,并且程序可以依赖用户已经授权的权限继续运行。(这里可能比较绕,举个例子:假设你的APP需要联系人和拍照权限,在请求权限时用户只授予了联系人权限,那么当前程序可以正常运行并获取联系人信息,但是无法进行拍照)

注:本篇文章讲解如何在API level 23或更高版本并且设备版本为Android6.0或者更高。如果APP的targetSDKVersion为22 或者更低,系统会在安装或者更新程序时提示用户授权所有敏感权限。

这里介绍下几个常量:

targetSdkVersion:是在程序运行的时候起作用,用于提高指定版本的设备上程序运行体验。

minSdkVersion和maxSdkVersion:是在程序安装的时候起作用,用于指定哪些版本的设备可以安装此应用。

targetAPIleve:是在编译的时候起作用,用于指定使用哪个API版本(SDK版本)进行编译。


4.1 PROTECTION_NORMAL类权限

当用户安装或更新应用时,系统将授予应用所请求的属于PROTECTION_NORMAL的所有权限(安装时授权的一类基本权限),这类权限包括:查看图片

只需要在AndroidManifest.xml中简单声明这些权限就好,安装时就授权。不需要每次使用时都检查权限,而且用户不能取消以上授权。


4.2 权限组

权限被分组了,如下表:

查看图片

同一组的任何一个权限被授权了,其他权限也自动被授权。例如,一旦WRITE_CONTACTS被授权了,APP也有READ_CONTACTS和GET_ACCOUNTSG权限了。


4.3 检查权限

如果你的程序需要敏感权限,那么你必须在每次调用需要该权限的方法时都需要检查权限。因为用户随时都可能会对你程序的某些权限取消授权,所以即使你的应用昨天使用过相机,你也无法确定今天是否还有这个权限。

你可以通过ContextCompat.checkSelfPermission()方法来验证你的应用是否拥有某个权限。比如,下面的代码段是检查是否有拥有写日历权限:


// 假设 thisActivity 是当前的 Activity

int permissionCheck = ContextCompat.checkSelfPermission(thisActivity,

        Manifest.permission.WRITE_CALENDAR);

如果该应用已经获取到该权限,该方法返回PackageManager.PERMISSION_GRANTED并且程序可以继续运行。如果该应用未被授予该权限,这个方法会返回PREMISSION_DENIED,同时应用需要明确提示用户该应用所需要的权限。

4.4 请求权限

如果你的应用需要敏感权限并且这些敏感权限已经在manifestm文件中声明,一定要询问用户获取权限。Android系统提供了几种请求权限的方法。调用这些方法后,系统会弹出一些Dialog(无需用户自定义)。


4.5 解释需要权限的原因

在一些应用场景下,你可能想要让用户知道需要获取某个权限的原因。例如,如果用户使用相册应用,用户可能会理解这个应用会需要相机权限,但是用户可能不会理解为什么相册应用还需要获取位置或者联系人。在你请求获取权限之前,你应该考虑提示用户。切记不要使用大量解释;如果你解释的内容过多,用户可能会觉得你的应用比较烦人,可能会卸载你的应用…(这段翻译可能有点问题…)

如果你需要的权限已经被用户拒绝过一次权限请求,当用户再次使用需要获取权限的功能时,应用程序最好向用户解释需要对应权限的原因。因为如果用户一直尝试使用需要权限的功能,却一直没给为该功能对应的权限,说明用户还没有明白为什么应用程序需要这个权限来实现这个功能。在这种情况下可能需要提示用户需要权限的原因。

Android 系统提供了shouldShowRequestPermissionRationale()方法来帮助开发者判断是否需要向用户解释需要权限的原因。当某条权限之前已经请求过,并且用户已经拒绝了该权限时,shouldShowRequestPermissionRationale ()方法返回的是true。

注意:如果用户拒绝某条权限,并且在提示授权的窗口中勾选了不再提示选项时,shouldShowRequestPermissionRationale ()的返回值为false。当某些设备禁止应用程序获取某些权限时,shouldShowRequestPermissionRationale ()也会返回false。


4.6 向用户请求获取应用程序需要的权限

如果你的应用程序没有获取到它需要的权限,那么应用程序需要调用该权限对应的requestPermissions()方法,调用requestPermissions()方法时需要传入一个请求码(requestCode),这时系统会弹出一个对话框让用户选择是否授权,用户选择后,在回调方法onRequestPermissionsResult()中返回对应的请求码(requestCode)和授权结果。

下面这段代码检查应用程序是否有读联系人权限,在未获取读联系人授权时请求获取该权限(完整示例见Android_M_Permission)

 

// thisActivity 为当前 Activity

// 检查是否已经授权该权限

if (ContextCompat.checkSelfPermission(thisActivity,

                Manifest.permission.READ_CONTACTS)

        != PackageManager.PERMISSION_GRANTED) {

 

    // 判断是否需要解释获取权限原因

    if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,

            Manifest。permission。READ_CONTACTS)) {

        // 需要向用户解释

        // 此处可以弹窗或用其他方式向用户解释需要该权限的原因

    } else {

        // 无需解释,直接请求权限

        ActivityCompat.requestPermissions(thisActivity,

                new String[]{Manifest。permission。READ_CONTACTS},

                MY_PERMISSIONS_REQUEST_READ_CONTACTS);

        // MY_PERMISSIONS_REQUEST_READ_CONTACTS 是自定义的常量,在回调方法中可以获取到

    }

}

注意:当应用程序调用requestPermissions()方法时,系统会弹出一个对话框给用户。应用程序不能设置或更改该对话框,如果应用程序需要提供一些信息或者向用户解释,需要在调用requestPermissions()方法之前。


4.7 处理请求权限的结果

当应用程序请求获取权限时,系统会弹出一个对话框给用户。当用户点击某个选项时,系统会调用onRequestPermissionResult()方法来传递用户的选择结果。应用程序需要重写onRequestPermissionsResult()方法来判断用户是否对相应权限授权。。这个回调方法会传递一个与requestPermission()方法相同的requestCode。例如,应用程序请求READ_CONTACTS方法,它将会有如下的回调方法:

@Override
public void onRequestPermissionsResult(int requestCode,
        String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
            //如果请求被取消,那么 result 数组将为空
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 已经获取对应权限
                // TODO 执行相应的操作联系人的方法
            } else {
                // 未获取到授权,取消需要该权限的方法
            }
            return;
        }
        // 检查其他权限....
    }
}  
授权的对话框显示的是系统描述的权限组(permission group),它没有显示列出详细的权限列表。比如,如果你请求READ_CONTACTS权限,系统对话框只会提示用户应用程序需要获取联系人权限,用户只需要给每个权限组授权一次。如果应用程序请求获取一个权限组的其他权限(在manifest文件中声明的权限),系统会自动授予该权限。当你请求这个权限时,系统会调用onRequestPermissionResult()回调方法并且传递PERMISSION_GRANTED,这跟用户在弹窗中点击授予权限的按钮的流程是相同的。

注意:应用程序还是需要明确的请求它所需要的每个权限,即使用户已经授予了跟这个权限在同一个permission group的其他权限。除此之外,对某个权限组的授权可能会改变。程序的代码不能依赖于用户已经对某个权限组授权的假设。

例如,应用程序在manifest 文件用声明了READ_CONTACTSWRITE_CONTACTS权限,如果应用程序请求了READ_CONTACTS权限并且用户授予了该权限,那么当应用程序请求WRITE_CONTACTS权限时,系统会自动授予应用程序该权限。

译者注:READ_CONTACTSWRITE_CONTACTS都属于CONTACTS权限组。更多关于权限组信息可以访问permission group或直接看我的截图:权限和权限组

如果用户拒绝了一个应用权限请求,那么应用程序应该进行适当的操作。例如:应用程序可以弹出一个对话框来解释为什么用户不能执行需要该权限的操作。

当系统提示用户给应用程序授权权限时,会给用户提供一个不再提示的选项来通知系统不再针对该权限进行询问。用户勾选该选项后,当应用程序请求获取对应权限时,系统会立即拒绝授权。系统会调用onRequestPermissionResult()回调方法并且传递PERMISSION_DENIED参数,就像用户拒绝授权一样。这意味着,当你调用requestPermissions()方法时,你不能假定应用程序会跟用户直接交互。


原理篇就介绍到这里,后续还有一个实践篇,有兴趣的请参阅~

 


 

长按指纹识别图中的二维码,获取更多测试干货分享!


查看图片