权限管理:Android权限模型演进、最小权限原则、运行时权限处理、自定义权限

聊到Android安全,权限管理绝对是个绕不开的话题。我做了这么多年安全开发,见过太多因为权限处理不当引发的安全事故。说白了,权限就是应用和系统之间的「通行证」——你拿到的权限越多,能做的事情就越多,但风险也越大。

今天咱们就把权限管理这块彻底讲透。从历史演进到实战编码,从系统权限到自定义权限,我会把我在项目中踩过的坑、总结的经验都分享出来。

Android权限模型的演进

Android的权限模型不是一天建成的。它经历了三个阶段,每个阶段都是为了解决上一阶段的问题。

版本 权限模型 核心特点
Android 1.0 - 5.1 安装时授权 安装时一次性授予,无法撤销
Android 6.0 - 9.0 运行时授权 用户可动态授予/撤销,支持权限分组
Android 10+ 精细化授权 新增仅限一次、后台位置等细粒度控制

我记得刚入行那会儿,Android 4.4还是主流。那时候权限管理特别粗暴——你安装应用时,系统会把所有权限列出来,用户要么全接受,要么不安装。很多用户根本看不懂那些权限列表,直接点「安装」就完事了。

这种模式的问题很明显。恶意应用可以申请一堆跟功能无关的权限,比如一个手电筒应用要读取联系人,用户也没办法拒绝。直到Android 6.0推出了运行时权限模型,才真正把控制权交还给了用户。

核心变化:从「安装时一次性授权」变成了「使用时按需授权」。用户可以在任何时候撤销某个权限,应用必须处理权限被拒绝的情况。

到了Android 10,权限管理又进了一步。新增了「仅限一次」选项,用户可以让应用只在使用期间访问位置或摄像头。Android 11更是加入了「自动重置权限」功能——如果用户长时间不用某个应用,系统会自动撤销之前授予的权限。

为什么会这样演进?说白了,用户隐私意识越来越强,监管也越来越严。Google必须不断收紧权限控制,才能应对层出不穷的隐私问题。

最小权限原则

最小权限原则,听起来很学术,其实道理很简单:只申请你真正需要的权限,一个都不要多

我在做代码审查时,经常看到这样的场景:一个简单的记事本应用,居然申请了读取联系人、访问位置、拍照权限。你想想看,用户会怎么想?「这破应用想干嘛?」

具体来说,最小权限原则包含三个层面:

  • 功能层面:每个权限必须对应一个明确的功能。如果某个功能可以不用权限实现,那就别申请。
  • 时机层面:只在需要的时候申请权限,不要在应用启动时一股脑全要了。
  • 范围层面:能用普通权限就别用危险权限,能用单次授权就别用始终允许。

我的习惯:每次写新功能前,我会先问自己三个问题:这个功能真的需要权限吗?有没有替代方案?如果用户拒绝权限,功能能不能降级运行?

举个例子。如果你的应用需要扫描Wi-Fi列表,很多人第一反应是申请ACCESS_FINE_LOCATION权限。但如果你只需要获取Wi-Fi名称和信号强度,其实可以用ACCESS_WIFI_STATE权限配合WifiManager来实现,完全不需要位置权限。

我曾经接手过一个项目,里面申请了十几个权限,但实际用到的只有五个。剩下的权限都是早期版本遗留的,或者某个开发人员「以防万一」加上的。清理掉这些冗余权限后,应用在Google Play的评分都涨了——因为用户看到权限列表变短了,信任感自然就上来了。

运行时权限处理

运行时权限是Android 6.0引入的核心机制。处理得好,用户体验流畅;处理不好,用户直接卸载。

先看一个标准的权限请求流程:

// 检查权限是否已授予
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        != PackageManager.PERMISSION_GRANTED) {
    // 是否需要向用户解释为什么需要这个权限
    if (ActivityCompat.shouldShowRequestPermissionRationale(this,
            Manifest.permission.CAMERA)) {
        // 显示解释对话框
        showRationaleDialog();
    } else {
        // 直接请求权限
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.CAMERA},
                REQUEST_CAMERA_PERMISSION);
    }
} else {
    // 权限已授予,直接执行操作
    openCamera();
}

这段代码看起来简单,但实际开发中有很多坑。我一个个说。

第一个坑:shouldShowRequestPermissionRationale的返回值

这个方法的返回值很多人搞不清楚。它返回true的情况只有一种:用户之前拒绝过权限请求,但没有勾选「不再询问」。如果用户第一次请求,或者用户勾选了「不再询问」,它都返回false。

所以正确的处理逻辑应该是:

  • 返回true → 用户拒绝过但没勾选「不再询问」→ 显示解释说明
  • 返回false且权限未授予 → 可能是第一次请求,也可能是用户勾选了「不再询问」→ 直接请求或引导用户去设置

第二个坑:不要一次性请求多个权限

我见过有些应用一上来就请求五六个权限,用户看到一堆弹窗,直接懵了。正确的做法是按需请求——用户要拍照时才请求相机权限,要发定位时才请求位置权限。

注意:如果用户连续拒绝两次权限请求,系统会自动把该权限标记为「不再询问」。之后你再调用requestPermissions,系统会直接拒绝,不会弹出任何对话框。这时候你必须引导用户去系统设置里手动开启权限。

第三个坑:权限被拒绝后的降级处理

很多应用在用户拒绝权限后直接崩溃或闪退,这是最糟糕的体验。你应该做的是:

@Override
public void onRequestPermissionsResult(int requestCode,
        @NonNull String[] permissions,
        @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_CAMERA_PERMISSION) {
        if (grantResults.length > 0 
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 权限授予,打开相机
            openCamera();
        } else {
            // 权限被拒绝,降级处理
            showToast("无法使用相机,已切换为相册选择");
            openGallery();
        }
    }
}

我曾经做过一个扫码应用,用户拒绝相机权限后,我们自动切换到手动输入条码的模式。虽然体验差了点,但至少应用还能用。很多用户后来觉得手动输入太麻烦,反而主动去开启了权限。

自定义权限

自定义权限是Android提供的一个高级特性,主要用于应用之间的安全通信。如果你的应用暴露了某些组件给其他应用调用,自定义权限可以控制谁有资格调用。

定义自定义权限需要在AndroidManifest.xml中声明:

<permission
    android:name="com.example.myapp.permission.MY_CUSTOM_PERMISSION"
    android:label="@string/permission_label"
    android:description="@string/permission_description"
    android:protectionLevel="signature" />

protectionLevel有几种取值,我列个表方便你对照:

保护级别 说明 使用场景
normal 低风险,安装时自动授予 不涉及用户隐私的权限
dangerous 高风险,需要运行时请求 涉及用户隐私的权限
signature 仅同签名应用可获取 自家应用之间的通信
signatureOrSystem 同签名或系统应用可获取 系统级应用通信

我个人最常用的是signature级别。为什么?因为同签名意味着这些应用都是你自家开发的,安全性有保障。而且用户不需要做任何操作,权限会自动授予。

举个例子。我做过一个电商应用,包含主App和支付插件两个APK。支付插件暴露了一个支付Activity,主App需要调用它。这时候自定义权限就派上用场了:

<!-- 支付插件中声明权限 -->
<permission
    android:name="com.example.shop.permission.PAY"
    android:protectionLevel="signature" />

<!-- 支付插件中保护Activity -->
<activity
    android:name=".PayActivity"
    android:permission="com.example.shop.permission.PAY">
    <intent-filter>
        <action android:name="com.example.shop.action.PAY" />
    </intent-filter>
</activity>

<!-- 主App中声明使用权限 -->
<uses-permission android:name="com.example.shop.permission.PAY" />

这样配置后,只有跟支付插件使用相同签名的应用才能调用PayActivity。其他应用即使知道这个Activity的包名和类名,也无法启动它。

避坑指南:我曾经犯过一个错误——把自定义权限的protectionLevel设成了dangerous。结果用户每次调用支付功能时都要弹窗确认,体验极差。后来改成signature级别,问题就解决了。所以记住:自家应用之间的通信,用signature就够了。

另外要注意一点:自定义权限的命名一定要用反向域名格式,比如com.example.app.permission.XXX。这样能避免跟其他应用的权限冲突。我见过有人直接用「MY_PERMISSION」这种名字,结果跟另一个应用的权限重名了,导致各种奇怪的问题。

知识体系总览

下面这张图把权限管理的核心知识点串起来了,你可以对照着回顾一下:

Android 权限管理知识体系 权限模型演进 安装时授权 → 运行时授权 → 精细化授权 (Android 1.0 → 6.0 → 10+) 最小权限原则 功能对应权限 按需申请 范围最小化 不多申请一个权限 运行时权限处理 检查 → 请求 → 回调 Rationale 解释 降级处理 拒绝后不崩溃 自定义权限 声明权限 保护级别 组件保护 同签名通信 核心目标:保护用户隐私 + 保障应用功能正常 权限不是越多越好,够用就行 实践建议 ① 优先使用普通权限 ② 运行时权限必须降级处理 ③ 自定义权限用signature级别 ④ 定期清理冗余权限 ⑤ 权限申请时机要合理

权限管理说难不难,说简单也不简单。关键在于你有没有把用户隐私放在第一位。我见过太多应用因为权限问题被下架、被投诉、被差评。其实只要遵循最小权限原则,做好运行时权限的降级处理,大部分问题都能避免。

嗯,关于权限管理就讲到这里。记住一句话:权限是能力,也是责任。拿到权限意味着你要对用户的数据负责,别滥用,别乱用。


公众号:蓝海资料掘金营,微信deep3321