基于android P平台权限管理详解

本文档详细介绍了Android P中的权限管理系统,包括权限的分隔、应用如何声明和获取权限,以及不同权限级别的概念。重点讲解了正常权限和危险权限的区别,以及Android 6.0及以上版本中运行时权限的管理机制。此外,还讨论了权限组、特殊权限如SYSTEM_ALERT_WINDOW的管理,并提供了查看和管理权限的命令。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

[TOC]

# 一 概述

Android 是一个权限分隔的操作系统,在安装应用时,Android 为每个软件包提供唯一的系统标识(Linux 用户 ID 和组 ID)。此 ID 在软件包在该设备上的使用寿命期间保持不变。系统各部分也分隔为不同的标识。Linux 据此将不同的应用以及应用与系统分隔开来。

在默认情况下任何应用都没有权限执行对其他应用、操作系统或用户有不利影响的任何操作。这包括读取或写入用户的私有数据(例如联系人或电子邮件)、读取或写入其他应用程序的文件、执行网络访问、使设备保持唤醒状态等。

由于每个 Android 应用都是在进程沙盒中运行,因此应用必须显式共享资源和数据。它们的方法是声明需要哪些权限来获取基本沙盒未提供的额外功能。应用以静态方式声明它们需要的权限,然后 Android 系统提示用户同意。

例如,需要监控传入的短信的应用要指定:

```
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.app.myapp" >
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    ...
</manifest>
```

如果在应用清单中列出`正常权限`(即不会对用户隐私或设备操作造成很大风险的权限),系统会自动授予这些权限。如果列出`危险权限`(即可能影响用户隐私或设备正常操作的权限),系统会要求用户明确授予这些权限。Android 发出请求的方式取决于系统版本,而系统版本是应用的目标:

- 如果设备运行的是 `Android 6.0(API 级别 23)或更高版本`,并且应用的 `targetSdkVersion` 是 23 或更高版本,则应用在运行时向用户请求权限。用户可随时调用权限,因此应用在每次运行时均需检查自身是否具备所需的权限。

- 如果设备运行的是 `Android 5.1(API 级别 22)或更低版本`,并且应用的 `targetSdkVersion` 是 22 或更低版本,则系统会在用户安装应用时要求用户授予权限。如果将新权限添加到更新的应用版本,系统会在用户更新应用时要求授予该权限。用户一旦安装应用,他们撤销权限的唯一方式是卸载应用。

通常,权限失效会导致 `SecurityException` 被扔回应用。但不能保证每个地方都是这样。例如,`sendBroadcast(Intent)` 方法在数据传递到每个接收者时会检查权限,在方法调用返回后,即使权限失效,也不会收到异常。但在几乎所有情况下,权限失效会记入系统日志。

Android 系统提供了一些默认权限,任何应用都可定义并实施自己的权限。可能在程序运行期间的多个位置实施特定权限:

- 在调用系统时,防止应用执行某些功能;

- 在启动 `Activity` 时,防止应用启动其他应用的 `Activity`;

- 在发送和接收 `Broadcast` 时,控制谁可以接收您的 `Broadcast` ,谁可以向您发送 `Broadcast`;

- 在访问和操作 `ContentProvider` 时;

- 绑定至服务或启动 `Service`。

# 二 Android 权限

Android 系统提供的权限

> frameworks/base/core/res/AndroidManifest.xml

```xml {.line-numbers}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="android" coreApp="true" android:sharedUserId="android.uid.system"
    android:sharedUserLabel="@string/android_system_label">

    ...

    <!-- Used for runtime permissions related to contacts and profiles on this
        device. -->
    <permission-group android:name="android.permission-group.CONTACTS"
        android:icon="@drawable/perm_group_contacts"
        android:label="@string/permgrouplab_contacts"
        android:description="@string/permgroupdesc_contacts"
        android:request="@string/permgrouprequest_contacts"
        android:priority="100" />

    <!-- Allows an application to read the user's contacts data.
        <p>Protection level: dangerous
    -->
    <permission android:name="android.permission.READ_CONTACTS"
        android:permissionGroup="android.permission-group.CONTACTS"
        android:label="@string/permlab_readContacts"
        android:description="@string/permdesc_readContacts"
        android:protectionLevel="dangerous" />

    ...

    <permission android:name="android.permission.GET_ACCOUNTS"
        android:permissionGroup="android.permission-group.CONTACTS"
        android:protectionLevel="dangerous"
        android:description="@string/permdesc_getAccounts"
        android:label="@string/permlab_getAccounts" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>

    ...

</manifest>
```

定义权限可用的标签:

> frameworks/base/core/res/res/values/attrs_manifest.xml

```xml {.line-numbers}
<resources>

    ...

    <declare-styleable name="AndroidManifestPermission" parent="AndroidManifest">
        <!-- Required public name of the permission, which other components and
        packages will use when referring to this permission.  This is a string using
        Java-style scoping to ensure it is unique.  The prefix will often
        be the same as our overall package name, for example
        "com.mycompany.android.myapp.SomePermission". -->
        <attr name="name" />
        <attr name="label" />
        <attr name="icon" />
        <attr name="roundIcon" />
        <attr name="banner" />
        <attr name="logo" />
        <attr name="permissionGroup" />
        <attr name="description" />
        <attr name="request" />
        <attr name="protectionLevel" />
        <attr name="permissionFlags" />
    </declare-styleable>

    ...

    <declare-styleable name="AndroidManifestPermissionGroup" parent="AndroidManifest">
        <!-- Required public name of the permission group, permissions will use
        to specify the group they are in.  This is a string using
        Java-style scoping to ensure it is unique.  The prefix will often
        be the same as our overall package name, for example
        "com.mycompany.android.myapp.SomePermission". -->
        <attr name="name" />
        <attr name="label" />
        <attr name="icon" />
        <attr name="roundIcon" />
        <attr name="banner" />
        <attr name="logo" />
        <attr name="description" />
        <attr name="request" />
        <attr name="permissionGroupFlags" />
        <attr name="priority" />
    </declare-styleable>

    ...

<resources/>
```

简要介绍下 permission 几个主要标签的含义:

- `name`:必选,权限的名字,供 uses-permission 等标签使用;

    ```
    <attr name="name" format="string" />
    ```

- `label`:提示给用户的权限名;

    ```
    <attr name="label" format="reference|string" />
    ```

- `icon`:权限图标

    ```
    <attr name="icon" format="reference" />
    ```

- `permissionGroup`:权限组,系统定义的 dengerous 权限都是有权限组的

    ```
    <attr name="permissionGroup" format="string" />
    ```

- `description`:提示给用户的权限描述

    ```
    <attr name="description" format="reference" />
    ```

- `protectionLevel`:必选,分为 normal、dengerous、signature、signatureOrSystem

    - `normal`:风险较低的权限,任何应用都可以申请,在安装应用时,不会直接提示给用户,点击全部才会展示;
    
    - `dengerous`:风险较高的权限,任何应用都可以申请,使用时需要用户确认才能使用;

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

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

- `permissionFlags`:表明权限更多上下文的标识

    ```
        <!-- Flags indicating more context for a permission. -->
        <attr name="permissionFlags">
            <!-- Set to indicate that this permission allows an operation that
                 may cost the user money.  Such permissions may be highlighted
                when shown to the user with this additional information.  -->
            <flag name="costsMoney" value="0x0001" />
            <!-- Additional flag from base permission type: this permission has been
                 removed and it is no longer enforced. It shouldn't be shown in the
                 UI. Removed permissions are kept as normal permissions for backwards
                 compatibility as apps may be checking them before calling an API.
            -->
            <flag name="removed" value="0x2" />
        </attr>
    ```

可以使用以下命令查看当前设备的所有权限:

> adb shell pm list permissions

## 2.1 自动权限调整

随着时间的推移,平台中可能会加入新的限制,要想使用特定 API,您的应用可能必须请求之前不需要的权限。因为现有应用假设可随意获取这些 API 应用的访问权限,所以 Android 可能会将新的权限请求应用到应用清单,以免在新平台版本上中断应用。Android 将根据为 `targetSdkVersion` 属性提供的值决定应用是否需要权限。如果该值低于在其中添加权限的版本,则 Android 会添加该权限。

例如,API 级别 4 中加入了 `WRITE_EXTERNAL_STORAGE` 权限,用以限制访问共享存储空间。如果您的 `targetSdkVersion` 为 3 或更低版本,则会向更新 Android 版本设备上的应用添加此权限。

> 注意:如果某权限自动添加到应用,则即使您的应用可能实际并不需要这些附加权限,`Google Play` 上的应用列表也会列出它们。

为避免这种情况,并且删除您不需要的默认权限,请始终将 `targetSdkVersion` 更新至最高版本。

## 2.2 权限保护级别

系统权限分为几个保护级别。需要了解的两个最重要保护级别是`正常权限`和`危险权限`:

- `正常权限`涵盖应用需要访问其沙盒外部数据或资源,但对用户隐私或其他应用操作风险很小的区域。例如,设置时区的权限就是`正常权限`。如果应用声明其需要`正常权限`,系统会自动向应用授予该权限;

- `危险权限`涵盖应用需要涉及用户隐私信息的数据或资源,或者可能对用户存储的数据或其他应用的操作产生影响的区域。例如,能够读取用户的联系人属于`危险权限`。如果应用声明其需要`危险权限`,则用户必须明确向应用授予该权限。

    使用以下命令可以查看当前设备的所有`危险权限`:

    > adb shell pm list permissions -d -g

    ![image](res/dengerous_permissions.png)

- `签名权限`需要应用具有相同的签名认证,在应用安装时系统会自动授予相同签名的应用声明要使用的该权限。使用系统签名的权限,一般三方应用是无法使用的。Android 系统提供了特殊方式去使用这种权限,例如:
    
    ```xml {.line-numbers}
        <!-- Must be required by an {@link android.accessibilityservice.AccessibilityService},
         to ensure that only the system can bind to it.
         <p>Protection level: signature
        -->
        <permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
            android:protectionLevel="signature" />
    ```

## 2.3 特殊权限

有些权限其行为方式与`正常权限`及`危险权限`都不同。`SYSTEM_ALERT_WINDOW` 和 `WRITE_SETTINGS` 特别敏感,因此大多数应用不应该使用它们。如果某应用需要其中一种权限,必须在清单中声明该权限,并且发送请求用户授权的 intent。系统将向用户显示详细管理屏幕,以响应该 intent。

### 2.3.1 SYSTEM_ALERT_WINDOW

申请使用悬浮窗权限:

- 在清单文件中申请使用该权限;

    ```xml {.line-numbers}
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    ```

- `API 23` 之后判断是否拥有该权限,之前是默认开启的;

    ```java {.line-numbers}
    // 检查是否已经授予权限
    if (Settings.canDrawOverlays(this)) {
        // TODO
    } else {
        // 若未授权则请求权限
        getOverlayPermission();
    }
    ```

- 申请权限(API>=23)

    ```java {.line-numbers}
    // 请求悬浮窗权限
    @TargetApi(Build.VERSION_CODES.M)
    private void getOverlayPermission() {
        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
        intent.setData(Uri.parse("package:" + getPackageName()));
        startActivityForResult(intent, 0);
    }
    ```

- 在 `onActivityResult` 方法中再次判断并做相关处理;

### 2.3.2 WRITE_SETTINGS

申请读取或修改系统设置权限:

- 在清单文件中申请使用该权限;

    ```xml {.line-numbers}
    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
    ```

- `API 23` 之后判断是否拥有该权限,之前是默认开启的;

    ```java {.line-numbers}
    // 检查是否已经授予权限
    if (Settings.System.canWrite()) {
        // TODO
    } else {
        // 若未授权则请求权限
        getWriteSettingsPermission();
    }
    ```

- 申请权限(API>=23)

    ```java {.line-numbers}
    // 请求更改设置权限
    @TargetApi(Build.VERSION_CODES.M)
    private void getWriteSettingsPermission() {
        Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
        intent.setData(Uri.parse("package:" + getPackageName()));
        startActivityForResult(intent, 0);
    }
    ```

- 在 `onActivityResult` 方法中再次判断并做相关处理;

## 2.4 权限组

Android 系统中定义的所有`危险权限`都属于`权限组`。如果设备运行的是 `Android 6.0(API 级别 23)`,并且应用的 `targetSdkVersion` 是 23 或更高版本,则当用户请求`危险权限`时系统会发生以下行为:

- 如果应用请求其清单中列出的`危险权限`,而应用目前未获得该权限所在`权限组`中的任何权限,则系统会向用户显示一个对话框,描述应用要访问的`权限组`。对话框不描述该组内的具体权限。例如,如果应用请求 `READ_CONTACTS` 权限,系统对话框只说明该应用需要访问设备的联系信息。如果用户批准,系统将向应用授予其请求的权限;

- 如果应用请求其清单中列出的`危险权限`,而应用在同一`权限组`中已有另一项`危险权限`,则系统会立即授予该权限,而无需与用户进行任何交互。例如,如果某应用已经请求并且被授予了 `READ_CONTACTS` 权限,然后它又请求 `WRITE_CONTACTS`,系统将立即授予该权限。

任何权限都可属于一个`权限组`,包括`正常权限`和`应用定义的权限`。但`权限组`仅当权限危险时才影响用户体验。可以忽略`正常权限`的`权限组`。

如果设备运行的是 `Android 5.1(API 级别 22)或更低版本`,并且应用的 `targetSdkVersion` 是 22 或更低版本,则系统会在安装时要求用户授予权限。再次强调,系统只告诉用户应用需要的`权限组`,而不告知具体权限。

## 2.5 自定义权限及应用

应用可以使用 `<permission>` 定义自己的自定义权限,并通过定义 `<uses-permission>` 元素请求其他应用的自定义权限。具体可参考 Android 系统中定义的 permission 进行自定义。

可以通过 `AndroidManifest.xml` 应用高级权限,限制访问系统或应用的全部组件。要执行此操作,在所需的组件上包含 `android:permission` 属性,为用于控制访问它的权限命名。

- `Activity` 权限(应用于 `<activity>` 标签)限制谁可以启动相关的 `Activity`。在 `Context.startActivity()` 和 `Activity.startActivityForResult()` 时会检查权限;如果调用方没有所需的权限,则调用会抛出 `SecurityException`。

- `Service` 权限(应用于 `<service>` 标签)限制谁可以启动或绑定到相关的服务。在 `Context.startService()、Context.stopService()` 和 `Context.bindService()` 时会检查权限;如果调用方没有所需的权限,则调用会抛出 `SecurityException`。

- `BroadcastReceiver` 权限(应用于 `<receiver>` 标签)限制谁可以发送广播给相关的接收方。在 `Context.sendBroadcast()` 返回后检查权限,因为系统会尝试将提交的广播传递到指定的接收方。因此,权限失效不会导致向调用方抛回异常;只是不会传递该 intent。同样,可以向 `Context.registerReceiver()` 提供权限来控制谁可以广播到以编程方式注册的接收方。另一方面,可以在调用 `Context.sendBroadcast()` 时提供权限来限制允许哪些 `BroadcastReceiver` 对象接收广播。

- `ContentProvider` 权限(应用于 `<provider>` 标签)限制谁可以访问 `ContentProvider` 中的数据。(`ContentProvider` 有重要的附加安全工具可用,称为 `URI 权限`,将在后面介绍。)与其他组件不同,您可以设置两个单独的权限属性:`android:readPermission` 限制谁可以读取,`android:writePermission` 限制谁可以写入。请注意,如果 `ContentProvider` 有读取和写入权限保护,仅拥有写入权限并不表示您可以读取。第一次检索 `ContentProvider` 时将会检查权限(如果没有任何权限,将会抛出 `SecurityException`),对 `ContentProvider` 执行操作时也会检查权限。使用 `ContentResolver.query()` 需要拥有读取权限;使用 `ContentResolver.insert()、ContentResolver.update()、ContentResolver.delete()` 需要写入权限。在所有这些情况下,没有所需的权限将导致调用抛出 `SecurityException`。

### 2.5.1 其他权限应用

可对任何服务调用实施任意细化的权限。这可通过 `Context.checkCallingPermission()` 方法完成。使用所需的权限字符串调用,它将返回一个整数,表示权限是否已授予当前的调用进程。请注意,仅在执行从另一个进程传入的调用(通常是通过从服务发布的 IDL 界面或者指定给另一进程的某种其他方式完成)时才可使用此方法。

检查权限还有许多其他有用的方法。如果您有另一个进程的 `pid`,可以使用 `Context` 方法 `Context.checkPermission(String, int, int)` 检查针对该 `pid` 的权限。如果您有另一个应用的软件包名称,可以使用直接的 `PackageManager` 方法 `PackageManager.checkPermission(String, String)` 了解是否已为特定软件包授予特定权限。

### 2.5.2 URI 权限

到目前为止所述的是标准权限系统,`ContentProvider` 仅仅使用此系统通常是不够的。`ContentProvider` 可能需要通过读取和写入权限保护自己,而其直接客户端也需要将特定 `URI` 传给其他应用以便于它们运行。邮件应用中的附件是一个典型的示例。应通过权限保护对邮件的访问,因为这是敏感的用户数据。但是,如果将图像附件的 `URI` 提供给图像查看程序,该图像查看程序不会有打开附件的权限,因为它没有理由拥有访问所有电子邮件的权限。

此问题的解决方法是采用 `per-URI` 权限机制:在启动 `Activity` 或返回结果给 `Activity` 时,调用方可以设置 `Intent.FLAG_GRANT_READ_URI_PERMISSION` 和(或) `Intent.FLAG_GRANT_WRITE_URI_PERMISSION`。这将授予接收 `Activity` 权限访问 intent 中的特定数据 `URI`,而不管它是否具有访问 intent 对应的 `ContentProvider` 中数据的任何权限。

此机制支持常见的能力式模型,其中用户交互(打开附件、从列表中选择联系人等)驱动临时授予细化的权限。这是一项关键功能,可将应用所需的权限缩小至只与其行为直接相关的权限。

但授予细化的 `URI` 权限需要与拥有这些 `URI` 的 `ContentProvider` 进行一定的合作。强烈建议 `ContentProvider` 实施此功能,并且通过 `android:grantUriPermissions` 属性或 `<grant-uri-permissions>` 标签声明支持此功能。

在 `Context.grantUriPermission()、Context.revokeUriPermission()` 和 `Context.checkUriPermission()` 方法中可以找到更多信息。

## 2.6 运行时权限

从 `Android 6.0(API 级别 23)` 开始,用户开始在应用运行时向其授予权限,而不是在应用安装时授予。此方法可以简化应用安装过程,因为用户在安装或更新应用时不需要授予权限。它还让用户可以对应用的功能进行更多控制;例如,用户可以选择为相机应用提供相机访问权限,而不提供设备位置的访问权限。用户可以随时进入应用的“Settings”界面管理权限。

### 2.6.1 检查权限

如果应用需要`危险权限`,则每次执行需要这一权限的操作时都必须检查自己是否具有该权限。用户始终可以自由管理此权限,因此,即使应用昨天使用了相机,它不能假设自己今天仍具有该权限。

要检查是否具有某项权限,请调用 `ContextCompat.checkSelfPermission()` 方法。例如,以下代码段显示了如何检查 `Activity` 是否具有在日历中进行写入的权限:

```java {.line-numbers}
// Assume thisActivity is the current activity
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity,
        Manifest.permission.WRITE_CALENDAR);
```

如果应用具有此权限,方法将返回 `PackageManager.PERMISSION_GRANTED`,并且应用可以继续操作。如果应用不具有此权限,方法将返回 `PERMISSION_DENIED`,且应用必须明确向用户要求权限。

### 2.6.2 请求权限

如果应用需要应用清单中列出的`危险权限`,那么,它必须要求用户授予该权限。Android 提供了多种权限请求方式。调用这些方法将显示一个标准的 Android 对话框,不过不能对它们进行自定义。

- 解释应用为什么需要权限

    在某些情况下,可能需要帮助用户了解应用为什么需要某项权限。例如,如果用户启动一个摄影应用,用户对应用要求使用相机的权限可能不会感到吃惊,但用户可能无法理解为什么此应用想要访问用户的位置或联系人。在请求权限之前,不妨为用户提供一个解释。请记住,不需要通过解释来说服用户;如果提供太多解释,用户可能发现应用令人失望并将其移除。

    可以采用的一个方法是仅在用户已拒绝某项权限请求时提供解释。如果用户继续尝试使用需要某项权限的功能,但继续拒绝权限请求,则可能表明用户不理解应用为什么需要此权限才能提供相关功能。对于这种情况,比较好的做法是显示解释。

    为了帮助查找用户可能需要解释的情形,Android 提供了一个实用程序方法,即 `shouldShowRequestPermissionRationale()`。如果应用之前请求过此权限但用户拒绝了请求,此方法将返回 true。

    > 注:如果用户在过去拒绝了权限请求,并在权

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值