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_SMS和RECEIVE_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来说不现实)。
- 方案二: 使用
SmsManager的sendTextMessage()方法,并捕获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);
}
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版本上变化很大。你写代码时,最好把兼容性测试做到位,尤其是针对Android 8、10、13这几个分水岭版本。嗯,今天就聊到这里,希望这些经验能帮你少走弯路。