Android 带权限的广播

682 阅读7分钟

我正在参加「掘金·启航计划」

本文记录了一次失败的尝试。

在项目中,使用了广播进行进程间通信。这样确实可行,因为广播确实可以用于进程间通信,只要在 AndroidManifest 中将广播接收器的 exported 属性声明为 true,就能接收到其他应用发出的广播了:

<receiver
    android:name=".MyBroadcastReceiver"
    android:enabled="true"
    android:exported="true">
    ...
</receiver>

但使用广播通信是不太安全的。比如应用发出的广播可以被第三方应用接收到,又比如广播接收器中的逻辑可以被第三方应用发出的广播触发。

为了解决这样的问题,Android 为广播引入了权限机制。使用带权限的广播,可以让这个广播只能被带有同样权限的应用接收到。这个机制被称之为“带权限的发送”。

与此类似地,还有一种被称之为“带权限的接收”的机制。意思是本应用只接收带有指定权限的发送者发出的广播。

这两种保护机制还可以同时使用,也就是说,既在发送时指定接收者需要的权限,又在接收时限制发送者需要的权限。这种机制可以称之为“双向权限保护”。

本篇文章我们会逐个讲解这三种机制。在这之前,我们简单看一下普通的广播是怎么发送和接收的。

一、广播的发送和接收

首先,我创建了两个项目,一个命名为 BroadcastTestA,用来发送广播,包名为 com.example.broadcasttesta,另一个命名为 BroadcastTestB,用来接收广播。包名为 com.example.broadcasttestb

我没有采用 Sender 和 Receiver 的命名方式,因为作为一个 Demo,我觉得用 A 和 B 读起来反而更顺畅。读者需要记住 BroadcastTestA 用来发送广播,BroadcastTestB 用来接收广播。以下简称 A 和 B。

在 A 应用中,给 B 应用发送一条普通的广播:

sendBroadcast(Intent("com.example.action").apply {
    component = ComponentName("com.example.broadcasttestb", "com.example.broadcasttestb.MyBroadcastReceiver")
    putExtra("message", "message from A")
})

在这段代码中,我们将 Intent 的 action 指定为 com.example.action,然后通过 ComponentName 指定 B 应用的 MyBroadcastReceiver 类来接收此广播。在 Intent 中,我们还存了一个 key 为 message 的字符串。

接下来,在 B 应用中,定义广播接收器:

class MyBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        Log.d("~~~", "I received ${intent?.getStringExtra("message")}")
    }
}

然后在 AndroidManifest 中声明此接收器:

<receiver
    android:name=".MyBroadcastReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="com.example.action" />
    </intent-filter>
</receiver>

可以看到,这个接收器的 action 指定为 com.example.action,这和我们在 A 应用中发送的 Intent 中的 action 是一致的。在收到此广播后,取出 message 字符串,打印了一条日志。

这样就完成了一条广播的发送和接收。启动这两个应用,调用 A 应用发送广播的代码,就能在 Logcat 控制台看到这条日志了。

com.example.broadcasttestb D/~~~: I received message from A

这种实现方式不安全的地方就在于:

  • 发广播时,只是指定了包名、接收者和 action,任何符合条件的应用都能收到这条广播,容易被窃听。
  • 收广播时,没有判断这条广播从何而来,只要收到对应 action 的广播,就会执行相应的逻辑,容易被攻击。

广播权限机制就是用来解决普通广播【容易被窃听】和【容易被攻击】这两个问题的。

二、带权限的发送

在发送广播时,sendBroadcast 函数可以指定接收者的权限,比如:

sendBroadcast(Intent("com.example.action").apply {
    component = ComponentName("com.example.broadcasttestb", "com.example.broadcasttestb.MyBroadcastReceiver")
    putExtra("message", "message from A")
}, Manifest.permission.INTERNET)

这里我们在 sendBroadcast 的第二个参数中,传入 INTERNET 权限,表示具有 INTERNET 权限的接收者才能接收这条广播。

所以想要接收这条广播,B 应用必须在 AndroidManifest 文件中指定 INTERNET 权限:

<uses-permission android:name="android.permission.INTERNET"/>

除了使用系统权限外,我们还可以在 AndroidManifest 中使用 permission 标签自定义权限。

比如在 A 应用发送广播时,指定接收者需要有 com.example.receiverpermission 权限:

sendBroadcast(Intent("com.example.action").apply {
    component = ComponentName("com.example.broadcasttestb", "com.example.broadcasttestb.MyBroadcastReceiver")
    putExtra("message", "message from A")
}, "com.example.receiverpermission")

此时,B 应用如果想要接收这条广播,就必须在 AndroidManifest 中声明这个权限:

<permission android:name="com.example.receiverpermission" />
<uses-permission android:name="com.example.receiverpermission"/>

这样就完成了带权限的发送。类似于我们在写信时,需要写好收件人地址,防止信被送到错误的地方。

这里需要注意的是,自定义权限时,应用的安装顺序会影响运行结果。必须先安装定义权限的应用,再安装使用此权限的应用,否则自定义权限无法生效。这是因为应用的 normal 级权限是在安装时授予的,如果在安装时没有找到此权限的定义,这个权限就会授予失败。

并且,在 Android 7 上不允许重复定义权限,如果某个应用定义了重复的权限,安装时会报错:

Failure [INSTALL_FAILED_DUPLICATE_PERMISSION: Package ... attempting to redeclare permission > ... already owned by ...]

三、带权限的接收

前文说到,在接收广播时,可以设置本应用只接收带有指定权限的发送者发出的广播。

要实现这个功能,只需要在定义接收器时,指定相应的权限即可:

<receiver
    android:name=".MyBroadcastReceiver"
    android:enabled="true"
    android:exported="true"
    android:permission="android.permission.INTERNET">
    <intent-filter>
        <action android:name="com.example.action" />
    </intent-filter>
</receiver>

这里通过 android:permission 指定了发送者需要带有 android.permission.INTERNET 权限,这条广播才会被接收。

类似地,除了使用系统权限外,我们也可以使用自定义权限对发送者加以限制。

比如,在 B 的 AndroidManifest 中,声明发送者需要拥有 com.example.senderpermission 权限:

<receiver
    android:name=".MyBroadcastReceiver"
    android:enabled="true"
    android:exported="true"
    android:permission="com.example.senderpermission">
    <intent-filter>
        <action android:name="com.example.action" />
    </intent-filter>
</receiver>

此时,A 应用如果想要给 B 应用发送广播,必须在 AndroidManifest 中,声明此权限:

<permission android:name="com.example.senderpermission"/>
<uses-permission android:name="com.example.senderpermission" />

这样就完成了带权限的接收。类似儿歌中唱的那样,大灰狼敲门时,小兔子不开门,只有确认了是兔妈妈在敲门时才开门。

四、双向权限保护

所谓双向权限保护,就是既在发送方限制接收者需要拥有的权限,又在接收方限制发送者需要拥有的权限。

在发送方 A 应用的 AndroidManifest 中:

<permission android:name="com.example.senderpermission" />
<uses-permission android:name="com.example.senderpermission" />

这里我们定义并使用了 com.example.senderpermission 权限。

接下来,在 A 应用中发送带权限的广播:

sendBroadcast(Intent("com.example.action").apply {
    component = ComponentName("com.example.broadcasttestb", "com.example.broadcasttestb.MyBroadcastReceiver")
    putExtra("message", "message from A")
}, "com.example.receiverpermission")

这里指定了接收者必须拥有 com.example.receiverpermission 权限。

然后在 B 应用的 AndroidManifest 中:

<permission android:name="com.example.receiverpermission"/>
<uses-permission android:name="com.example.receiverpermission"/>

在 B 应用中定义并使用了 com.example.receiverpermission 权限。

接下来注册接收器:

<receiver
    android:name=".MyBroadcastReceiver"
    android:enabled="true"
    android:exported="true"
    android:permission="com.example.senderpermission">
    <intent-filter>
        <action android:name="com.example.action" />
    </intent-filter>
</receiver>

可以看到,这里给接收器指定了只接收带有 com.example.senderpermission 权限的发送者发来的广播。

这样就完成了双向权限保护。

六、权限保护等级

读到这里,细心的同学可能会问了。前文说到,广播权限机制是用来解决普通广播【容易被窃听】和【容易被攻击】这两个问题的。引入权限之后真的解决了这两个问题吗?

答案是目前为止并没有。第三方应用仍然可以通过声明接收者的权限继续窃听,也可以声明发送者的权限继续攻击。引入权限只是多了一道屏障,还是没有彻底解决广播的安全问题。

需要解决这两个问题,还需要引入权限保护等级的概念,即:protectionLevel 属性。

在定义权限时,可以指定权限的保护等级:

<permission android:name="com.example.permission" android:protectionLevel="normal"/>

权限等级的默认值为 normal,这类权限只要申请就能获取到。除此之外,还有以下常用的值:

  • signature:表示 apk 签名一致才能获取到
  • signatureOrSystem:表示 apk 签名一致或者系统应用才能获取到

现在我们就能解决广播的安全问题了。指定权限保护等级为 signature 后,应用在安装时,只有签名相同,才可以获取到此权限。在公司签名相同的产品族中就可以使用较安全的广播了,因为签名是无法伪造的。

七、后记

这篇文章的诞生是因为我在项目中看到了带权限的广播,所以研究了一下。但研究之后,我认为这种带权限的广播使用起来还是有其局限性,如果同公司的两个产品签名不一致,却需要通信呢?这种情况下使用广播始终是不安全的。

所以我并不推荐使用广播进行 IPC,它只在有限的场景下能保证安全。而且广播通信较难溯源,如果滥用的话,发送者很可能会不知道自己的广播发到了哪里去,接收者也很可能不知道自己收到的广播是哪里发来的(类似 EventBus)。所以说本文是一篇失败的尝试,AIDL 和 Messenger 才是 IPC 的正确方式。

八、参考文章

Android 官方文档-通过权限限制广播

Android 广播权限保护

Android Broadcast 和 BroadcastReceiver的权限限制方式