Android短信监控技术实现:合法合规的远程采集方案

Android短信监控技术实现:合法合规的远程采集方案

一年经验的全栈程序员,目前头发健在,但不知道能撑多久。

该项目已成功部署并稳定运行于企业生产环境,如需个性化定制方案,欢迎联系作者进行深度合作。

文章目录

前言

一、页面设计

1.页面显示

2.代码实现

二、具体代码实现

1.添加网络权限和短信权限

2.实现短信监听(BroadcastReceiver)

3.AndroidManifest.xml 中注册广播接收器

4. 封装网络请求(HttpURLConnection)

三、MainActivity主程序编写

1. 权限管理模块

2. 短信接收与处理模块

3. 数据存储与展示模块

4. 用户配置管理模块

5. 定时清理模块(可选)

总结

🙌 求点赞、收藏、关注!

前言

由于公司业务需求需要监控大批手机号的验证码所以有了这个项目,在 Android 应用开发中,短信监控通常用于合规场景,如企业设备管理、金融风控(验证码自动填充)或家长监护。不同于直接读取短信数据库(ContentProvider),使用 BroadcastReceiver 监听短信广播(android.provider.Telephony.SMS_RECEIVED)是一种更轻量、实时性更强的方案。

本文将介绍如何通过 BroadcastReceiver 捕获短信,并使用 原生 HttpURLConnection(而非第三方库)将数据安全上传至服务器,涵盖以下关键点:

短信监听机制:注册广播接收器,过滤有效短信(如特定发送方或验证码)。

网络请求实现:手动封装 HttpURLConnection,支持 POST/GET 请求。

安全与合规性:

动态申请 RECEIVE_SMS 权限,确保用户知情同意。

避免存储敏感信息,仅传输必要数据。

注意:未经用户授权的短信监控属于违法行为,本文仅限技术探讨,请确保应用符合 Google Play 政策及《个人信息保护法》。

一、页面设计

由于没有做统一的日志管理但是也需要查看短信是否有监听到使用页面需要显示监听的手机号和内容。

1.页面显示

2.代码实现

xmlns:app="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

tools:context=".MainActivity">

android:id="@+id/appNameTextView"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:padding="16dp"

android:text="短信接收器"

android:textSize="20sp"

android:textStyle="bold"

app:layout_constraintLeft_toLeftOf="parent"

app:layout_constraintRight_toRightOf="parent"

app:layout_constraintTop_toTopOf="parent" />

android:id="@+id/baoc"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginEnd="16dp"

android:onClick="save"

android:text="保存"

app:layout_constraintBottom_toBottomOf="@+id/appNameTextView"

app:layout_constraintRight_toRightOf="parent"

app:layout_constraintTop_toTopOf="@+id/appNameTextView" />

android:id="@+id/card1TitleTextView"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginStart="16dp"

android:layout_marginTop="16dp"

android:text="卡1:"

android:textSize="16sp"

android:textStyle="bold"

app:layout_constraintBottom_toBottomOf="@+id/editTextPhone1"

app:layout_constraintLeft_toLeftOf="parent"

app:layout_constraintTop_toBottomOf="@+id/appNameTextView" />

android:id="@+id/editTextPhone1"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layout_marginStart="8dp"

android:layout_marginEnd="8dp"

android:hint="输入卡1号码"

android:textSize="16sp"

app:layout_constraintLeft_toRightOf="@+id/card1TitleTextView"

app:layout_constraintRight_toLeftOf="@+id/switch1"

app:layout_constraintTop_toTopOf="@+id/card1TitleTextView" />

android:id="@+id/switch1"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginEnd="16dp"

app:layout_constraintBottom_toBottomOf="@+id/editTextPhone1"

app:layout_constraintRight_toRightOf="parent"

app:layout_constraintTop_toTopOf="@+id/editTextPhone1" />

android:id="@+id/card2TitleTextView"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginStart="16dp"

android:layout_marginTop="16dp"

android:text="卡2:"

android:textSize="16sp"

android:textStyle="bold"

app:layout_constraintBottom_toBottomOf="@+id/editTextPhone2"

app:layout_constraintLeft_toLeftOf="parent"

app:layout_constraintTop_toBottomOf="@+id/editTextPhone1" />

android:id="@+id/editTextPhone2"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layout_marginStart="8dp"

android:layout_marginEnd="8dp"

android:hint="输入卡2号码"

android:textSize="16sp"

app:layout_constraintLeft_toRightOf="@+id/card2TitleTextView"

app:layout_constraintRight_toLeftOf="@+id/switch2"

app:layout_constraintTop_toTopOf="@+id/card2TitleTextView" />

android:id="@+id/switch2"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginEnd="16dp"

app:layout_constraintBottom_toBottomOf="@+id/editTextPhone2"

app:layout_constraintRight_toRightOf="parent"

app:layout_constraintTop_toTopOf="@+id/editTextPhone2" />

android:id="@+id/smsTextView"

android:layout_width="0dp"

android:layout_height="300dp"

android:layout_marginTop="8dp"

android:background="#000000"

android:padding="8dp"

android:text="监控短信中"

android:textColor="#FFFFFF"

android:scrollbars="vertical"

app:layout_constraintLeft_toLeftOf="parent"

app:layout_constraintRight_toRightOf="parent"

app:layout_constraintTop_toBottomOf="@+id/editTextPhone2" />

为什么这里需要设置卡号显示起除是因为安卓开发系统的匹配度问题有些手机系统是不能自动识别手机号的所以显示这个卡号是看看有没有自动识别,如果没有需要手动输入并且保存,以为后面是根据具体卡槽id识别是哪个手机号接收到的验证码。

二、具体代码实现

1.添加网络权限和短信权限

在AndroidManifest.xml

2.实现短信监听(BroadcastReceiver)

新建一个SMSReceiver.class服务监听短信

public class SMSReceiver extends BroadcastReceiver {

@Override

public void onReceive(Context context, Intent intent) {

// 1. 从广播意图中提取短信数据

Bundle bundle = intent.getExtras();

if (bundle != null) {

// 2. 获取短信PDU数组和SIM卡订阅ID

Object[] pdus = (Object[]) bundle.get("pdus");

int subscriptionId = bundle.getInt("subscription", -1);

if (pdus != null) {

for (Object pdu : pdus) {

// 3. 解析短信内容(发送方、正文)

SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) pdu);

String messageBody = smsMessage.getMessageBody();

String sender = smsMessage.getDisplayOriginatingAddress();

// 4. 获取SIM卡槽位信息(双卡场景)

String slotInfo = getSlotInfo(context, subscriptionId);

// 5. 发送自定义广播传递短信数据

Intent smsIntent = new Intent("smsReceived");

smsIntent.putExtra("message", messageBody);

smsIntent.putExtra("sender", sender);

smsIntent.putExtra("slotInfo", slotInfo);

context.sendBroadcast(smsIntent);

}

}

}

}

/**

* 获取SIM卡槽位信息(兼容双卡)

* @param subscriptionId SIM卡订阅ID

* @return 如 "Slot 1" 或 "Unknown Slot"

*/

private String getSlotInfo(Context context, int subscriptionId) {

// 实现逻辑:通过SubscriptionManager查询对应SIM卡槽

// ...

}

/**

* 获取接收方手机号(需权限)

* @note 因权限问题已注释,实际使用需动态申请READ_PHONE_STATE权限

*/

@RequiresApi(api = Build.VERSION_CODES.N)

private String getReceiverPhoneNumber(Context context, int subscriptionId) {

// 实现逻辑:通过TelephonyManager获取本机号码

// ...

}

}

3.AndroidManifest.xml 中注册广播接收器

android:exported="true"

android:enabled="true">

4. 封装网络请求(HttpURLConnection)

public class MyRequest {

public String post(String url1, String data) {

try {

URL url = new URL(url1);

HttpURLConnection Connection = (HttpURLConnection) url.openConnection();//创建连接

Connection.setRequestMethod("POST");

Connection.setConnectTimeout(3000);

Connection.setReadTimeout(3000);

Connection.setDoInput(true);

Connection.setDoOutput(true);

Connection.setUseCaches(false);

Connection.connect();

DataOutputStream dos = new DataOutputStream(Connection.getOutputStream());

String title = data;//这里是POST请求需要的参数字符串类型,例如"id=1&data=2"

dos.write(title.getBytes());

dos.flush();

dos.close();//写完记得关闭

int responseCode = Connection.getResponseCode();

if (responseCode == Connection.HTTP_OK) {//判断请求是否成功

InputStream inputStream = Connection.getInputStream();

ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();

byte[] bytes = new byte[1024];

int length = 0;

while ((length = inputStream.read(bytes)) != -1) {

arrayOutputStream.write(bytes, 0, length);

arrayOutputStream.flush();

}//读取响应体的内容

String s = arrayOutputStream.toString();

return s;//返回请求到的内容,字符串形式

} else {

return "-1";//如果请求失败返回-1

}

} catch (Exception e) {

return "-1";//出现异常也返回-1

}

}

public String get(String url1) {

try {

URL url = new URL(url1);

HttpURLConnection Connection = (HttpURLConnection) url.openConnection();

Connection.setRequestMethod("GET");

Connection.setConnectTimeout(3000);

Connection.setReadTimeout(3000);

int responseCode = Connection.getResponseCode();

if (responseCode == Connection.HTTP_OK) {

InputStream inputStream = Connection.getInputStream();

ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();

byte[] bytes = new byte[1024];

int length = 0;

while ((length = inputStream.read(bytes)) != -1) {

arrayOutputStream.write(bytes, 0, length);

arrayOutputStream.flush();//强制释放缓冲区

}

String s = arrayOutputStream.toString();

return s;

} else {

return "-1";

}

} catch (Exception e) {

return "-1"+e;

}

}

}

三、MainActivity主程序编写

1. 权限管理模块

// 定义所需权限

private static final int PERMISSION_REQUEST_CODE = 1;

String[] permissions = {

Manifest.permission.READ_SMS,

Manifest.permission.RECEIVE_SMS,

Manifest.permission.READ_PHONE_STATE,

Manifest.permission.READ_PHONE_NUMBERS

};

// 检查并请求权限

if (未全部授权) {

ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE);

} else {

registerSmsReceiver(); // 注册广播接收器

}

// 权限请求结果回调

@Override

public void onRequestPermissionsResult(...) {

if (权限通过) {

registerSmsReceiver();

} else {

showToast("短信监控功能不可用");

}

}

2. 短信接收与处理模块

// 自定义广播接收器

private BroadcastReceiver smsReceiver = new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

// 解析短信数据

String message = intent.getStringExtra("message");

String sender = intent.getStringExtra("sender");

String slotInfo = intent.getStringExtra("slotInfo");

// 根据SIM卡槽匹配接收号码

String receiverNumber = slotInfo.contains("1") ?

editTextPhone1.getText().toString() :

editTextPhone2.getText().toString();

// 过滤重复消息

if (pdMessages(message)) {

// 启动网络请求线程

new Thread(() -> {

String url = "服务器接口;

String response = new MyRequest().get(url);

handleResponse(response, receiverNumber, sender, message);

}).start();

}

}

};

// 消息去重检查

private boolean pdMessages(String mess) {

Set savedMessages = sp.getStringSet("messages", new HashSet<>());

if (savedMessages.contains(mess)) {

return false; // 重复消息

}

savedMessages.add(mess);

sp.edit().putStringSet("messages", savedMessages).apply();

return true;

}

3. 数据存储与展示模块

// 存储消息记录(包含状态和时间戳)

private void storeMessageStatus(String receiver, String sender, String message, String status) {

String key = "message_" + System.currentTimeMillis();

String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());

String record = String.format(

"接收号: %s\n发送方: %s\n时间: %s\n内容: %s\n状态: %s\n",

receiver, sender, time, message, status

);

sp.edit()

.putString(key + "_data", record)

.putLong(key + "_time", System.currentTimeMillis())

.apply();

}

// 显示历史消息

private void displayStoredMessages() {

StringBuilder sb = new StringBuilder();

for (Map.Entry entry : sp.getAll().entrySet()) {

if (entry.getKey().endsWith("_data")) {

sb.append(entry.getValue()).append("\n");

}

}

smsTextView.setText(sb.length() > 0 ? sb.toString() : "无记录");

}

4. 用户配置管理模块

// 保存用户设置的手机号

public void save(View view) {

sp.edit()

.putString("editTextPhone1", editTextPhone1.getText().toString())

.putString("editTextPhone2", editTextPhone2.getText().toString())

.apply();

showToast("保存成功");

}

// 初始化时加载配置

private void loadConfig() {

editTextPhone1.setText(sp.getString("editTextPhone1", ""));

editTextPhone2.setText(sp.getString("editTextPhone2", ""));

}

5. 定时清理模块(可选)

// 设置每天中午12点清理旧数据

private void setDailyCleanupAlarm() {

AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);

Intent intent = new Intent(this, CleanupReceiver.class);

PendingIntent pendingIntent = PendingIntent.getBroadcast(...);

Calendar calendar = Calendar.getInstance();

calendar.set(Calendar.HOUR_OF_DAY, 12); // 12:00 PM

if (已过当前时间) calendar.add(Calendar.DAY_OF_YEAR, 1);

alarmManager.setExactAndAllowWhileIdle(

AlarmManager.RTC_WAKEUP,

calendar.getTimeInMillis(),

pendingIntent

);

}

总结

实现了双卡短信监控:通过BroadcastReceiver捕获短信,并根据SIM卡槽自动匹配预设的手机号,支持双卡场景下的短信分类处理。

数据安全与合规性:动态申请权限确保用户知情权,使用SharedPreferences存储消息记录,避免敏感信息泄露,符合隐私保护要求。

网络上传与状态反馈:通过HttpURLConnection将短信内容上传至服务器,并实时显示发送状态(成功/失败),数据持久化便于追溯。

可扩展性强:模块化设计(权限管理、消息处理、数据存储)便于后续扩展,如增加加密传输或对接其他API。

🙌 求点赞、收藏、关注!

如果这篇文章对你有帮助,不妨: 👍 点个赞 → 让更多人看到这篇干货! ⭐ 收藏一下 → 方便以后随时查阅! 🔔 加关注 → 获取更多 前端/后端/全栈技术深度解析!

你的支持,是我持续创作的最大动力! 🚀

相关推荐

国家移民管理局政务服务平台
hse365平台

国家移民管理局政务服务平台

📅 07-02 👁️ 8339
非洲有多少个国家?
office365ios版本

非洲有多少个国家?

📅 08-19 👁️ 1466
玫瑰小镇任务 嫁接鲜花和玫瑰的时间表
365BET体育投注官网

玫瑰小镇任务 嫁接鲜花和玫瑰的时间表

📅 07-22 👁️ 5515
大运交接与人生凶灾
365BET体育投注官网

大运交接与人生凶灾

📅 06-30 👁️ 6520