21、权限与短信:SEND_SMS、RECEIVE_SMS、READ_SMS权限限制

短信权限,在Android里一直是个敏感地带。我刚开始做安全审计那会儿,经常看到一些App明明就是个手电筒,却申请了SEND_SMS权限。你想想看,这合理吗?显然不合理。Google后来在Android 6.0引入了运行时权限,短信权限被划入了危险权限组,必须动态申请。今天我们就来彻底聊透这三个短信相关权限。

三个权限,各管一摊

先搞清楚它们分别管什么:

权限名 对应操作 典型场景
SEND_SMS 发送短信 验证码自动发送、短信登录
RECEIVE_SMS 接收短信(监听广播) 短信验证码自动填充
READ_SMS 读取已存在的短信 短信备份、账单识别

说白了,SEND_SMS是往外掏钱,RECEIVE_SMS是偷听新消息,READ_SMS是翻旧账。这三个权限一旦被滥用,用户的隐私和财产都会受到威胁。我个人习惯把短信权限称为“高危三件套”,因为它们直接关联到用户的通信数据和资费。

运行时申请的正确姿势

从Android 6.0(API 23)开始,这三个权限都需要在运行时动态申请。嗯,这里要注意:SEND_SMSRECEIVE_SMS虽然都属于短信权限组,但它们是独立的权限,不能用一个请求同时拿两个。我见过不少新手在requestPermissions里只传一个权限,结果另一个没拿到,代码就崩了。

正确的做法是这样:

// 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS)
        != PackageManager.PERMISSION_GRANTED) {
    // 解释为什么需要这个权限(可选)
    if (ActivityCompat.shouldShowRequestPermissionRationale(this,
            Manifest.permission.SEND_SMS)) {
        // 显示一个解释对话框
        showRationaleDialog();
    } else {
        // 直接请求
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.SEND_SMS},
                REQUEST_CODE_SEND_SMS);
    }
} else {
    // 权限已授予,直接发送
    sendSms();
}
我的小技巧: 我习惯把权限请求码定义成常量,比如REQUEST_CODE_SEND_SMS = 1001,这样在onRequestPermissionsResult里好区分。千万别用魔法数字,否则代码维护起来很痛苦。

避坑指南:那些年我踩过的雷

我曾经在一个金融类App里遇到过一个问题:用户明明授予了SEND_SMS权限,但发送短信时却抛出了SecurityException。查了半天,发现是系统短信应用的坑。从Android 4.4开始,只有默认短信应用才能直接调用SmsManager.sendTextMessage()。如果你的App不是默认短信应用,即使有SEND_SMS权限,也会被系统拦截。

解决方案有两种:

  • 方案一: 引导用户将你的App设为默认短信应用(但这对大多数App来说不现实)。
  • 方案二: 使用SmsManagersendTextMessage()方法,并捕获SecurityException,然后降级为调用系统短信Intent。

我个人更推荐方案二,因为用户体验更好。代码大概长这样:

try {
    SmsManager smsManager = SmsManager.getDefault();
    smsManager.sendTextMessage(phoneNumber, null, message, null, null);
} catch (SecurityException e) {
    // 降级:使用Intent调起系统短信
    Intent intent = new Intent(Intent.ACTION_SENDTO);
    intent.setData(Uri.parse("smsto:" + phoneNumber));
    intent.putExtra("sms_body", message);
    startActivity(intent);
}
注意: 从Android 10(API 29)开始,非默认短信应用调用SmsManager会直接抛出SecurityException,不再静默失败。所以这个try-catch是必须的,别偷懒。

RECEIVE_SMS 的监听陷阱

RECEIVE_SMS权限允许你的App监听新收到的短信。很多App用它来做验证码自动填充。但这里有个坑:从Android 8.0(API 26)开始,系统对隐式广播做了限制。你如果还在清单文件里静态注册android.provider.Telephony.SMS_RECEIVED广播接收器,会发现收不到广播。

为什么会这样?因为系统认为短信广播是敏感事件,不允许后台App随意监听。解决办法是改用动态注册

// 在Activity或Service中动态注册
IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
registerReceiver(smsReceiver, filter);

// 别忘了在onDestroy或onPause中取消注册
unregisterReceiver(smsReceiver);

另外,如果你的App是默认短信应用电话应用,可以静态注册。否则,老老实实动态注册吧。我记得有一次帮客户排查问题,他们静态注册死活收不到短信,改成动态注册后立刻就好了。嗯,这个坑踩过的人应该不少。

READ_SMS 的隐私红线

READ_SMS权限允许读取设备上已有的短信内容。这个权限非常敏感,因为短信里可能包含验证码、银行通知、甚至私人对话。Google在Android 10中进一步收紧了限制:即使你有READ_SMS权限,也不能读取其他App(比如系统短信App)的短信数据库,除非你的App是默认短信应用。

说白了,READ_SMS权限现在主要留给短信备份类App使用。普通App如果只是想读取验证码,建议使用RECEIVE_SMS监听新短信,而不是去翻旧短信。我在做安全审计时,经常建议客户:如果不需要读取历史短信,就别申请READ_SMS,否则上架Google Play时会被拒。

核心原则: 最小权限原则。只申请你真正需要的权限,并且在使用时动态申请,不要提前申请。用户拒绝后,要能优雅降级,而不是直接闪退。

知识体系一览

下面这张图总结了短信权限的核心逻辑和限制条件,我画成了流程图,方便你理解:

短信权限核心逻辑 SEND_SMS 发送短信 RECEIVE_SMS 监听新短信 READ_SMS 读取历史短信 限制条件 非默认短信App Android 10+ 抛出异常 限制条件 Android 8+ 需动态注册 默认应用可静态注册 限制条件 Android 10+ 仅默认应用 可读取历史短信 最佳实践建议 动态申请 · 最小权限 · 优雅降级 · 捕获异常

总结一下

短信权限的管理,核心就三点:

  • 动态申请: 别在清单文件里静态声明就完事了,运行时一定要检查并请求。
  • 区分场景: 发送用SEND_SMS,监听新短信用RECEIVE_SMS,读历史用READ_SMS。别混用。
  • 注意系统限制: 非默认短信App在Android 10+上发送短信会抛异常,接收短信在Android 8+上需要动态注册。

我个人觉得,短信权限是运行时权限里最容易出问题的之一,因为它的行为在不同Android版本上变化很大。你写代码时,最好把兼容性测试做到位,尤其是针对Android 8、10、13这几个分水岭版本。嗯,今天就聊到这里,希望这些经验能帮你少走弯路。

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