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(而非第三方库)将数据安全上传至服务器,涵盖以下关键点:

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

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

  3. 安全与合规性

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

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

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


一、页面设计

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

1.页面显示

 2.代码实现

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"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"><!-- 应用名称标题 --><TextViewandroid: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" /><!-- 卡1标题 --><Buttonandroid: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" /><TextViewandroid: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" /><!-- 卡1输入框 --><EditTextandroid: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" /><!-- 卡1开关 --><Switchandroid: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" /><!-- 卡2标题 --><TextViewandroid: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" /><!-- 卡2输入框 --><EditTextandroid: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" /><!-- 卡2开关 --><Switchandroid: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" /><!-- 短信内容显示框 --><TextViewandroid: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" /></androidx.constraintlayout.widget.ConstraintLayout>

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


 二、具体代码实现

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

在AndroidManifest.xml

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

2.实现短信监听(BroadcastReceiver)

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

public class SMSReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {// 1. 从广播意图中提取短信数据Bundle bundle = intent.getExtras();if (bundle != null) {// 2. 获取短信PDU数组和SIM卡订阅IDObject[] 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 中注册广播接收器

 <receiver android:name=".SMSReceiver"android:exported="true"android:enabled="true"><intent-filter><action android:name="android.provider.Telephony.SMS_RECEIVED"/></intent-filter></receiver>

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() {@Overridepublic 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<String> 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<String, ?> 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 PMif (已过当前时间) calendar.add(Calendar.DAY_OF_YEAR, 1);alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
}

 总结

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

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

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

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

🙌 求点赞、收藏、关注! 

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/78360.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端跨域问题怎么在后端解决

目录 简单的解决方法&#xff1a; 添加配置类&#xff1a; 为什么会跨域 1. 什么是源 2. URL结构 3. 同源不同源举&#x1f330; 同源例子 不同源例子 4. 浏览器为什么需要同源策略 5. 常规前端请求跨域 简单的解决方法&#xff1a; 添加配置类&#xff1a; packag…

【中间件】brpc_基础_execution_queue

execution_queue 源码 1 简介 execution_queue.h 是 Apache BRPC 中实现 高性能异步任务执行队列 的核心组件&#xff0c;主要用于在用户态线程&#xff08;bthread&#xff09;中实现任务的 异步提交、有序执行和高效调度。 该模块通过解耦任务提交与执行过程&#xff0c;提…

java学习之数据结构:一、数组

主要是对数组所有的东西进行总结&#xff0c;整理 适合小白~ 目录 1.什么是数组 1.1数组定义 1.2数组创建 1&#xff09;静态创建 2&#xff09;动态创建 1.3数组遍历 1&#xff09;for和while遍历 2&#xff09;foreach遍历 2.数组越界问题及解决 2.1数组越界问题 2…

[Survey]SAM2 for Image and Video Segmentation: A Comprehensive Survey

BaseInfo TitleSAM2 for Image and Video Segmentation: A Comprehensive SurveyAdresshttps://arxiv.org/abs/2503.12781Journal/Time2503Author四川大学&#xff0c;北京大学 1. Introduction 图像分割专注于识别单个图像中的目标、边界或纹理&#xff0c;而视频分割则将这…

用Maven定位和解决依赖冲突

用Maven定位和解决依赖冲突 一、依赖冲突的常见表现二、定位冲突依赖的4种方法2.1 使用Maven命令分析依赖树2.2 使用IDE可视化工具2.3 使用Maven Enforcer插件2.4 运行时分析 三、解决依赖冲突的5种方案3.1 排除特定传递依赖3.2 统一指定版本&#xff08;推荐&#xff09;3.3 使…

穿越数据森林与网络迷宫:树与图上动态规划实战指南

在 C 算法的浩瀚宇宙中&#xff0c;树与图就像是神秘的迷宫和茂密的森林&#xff0c;充满了未知与挑战。而动态规划则是我们探索其中的神奇罗盘&#xff0c;帮助我们找到最优路径。今天&#xff0c;就让我们一起深入这片神秘领域&#xff0c;揭开树与图上动态规划的神秘面纱&am…

UDP / TCP 协议

目录 一、前言&#xff1a; 数据封装与分用&#xff1a; 二、网络协议分层模型&#xff1a; 三、UDP / TCP 协议 UDP 协议&#xff1a; 1、UDP 协议段格式&#xff1a; 2、UDP 的特点&#xff1a; TCP 协议&#xff1a; 1、TCP 协议段格式&#xff1a; 2、TCP 协议的十…

Python 实现的运筹优化系统数学建模详解(动态规划模型)

相关代码链接&#xff1a;https://download.csdn.net/download/heikediguoshinib/90713747?spm1001.2014.3001.5503 一、引言 在计算机科学与数学建模的广阔领域中&#xff0c;算法如同精密的齿轮&#xff0c;推动着问题的解决与系统的运行。当面对复杂的优化问题时&…

langfuse本地安装

目录 安装命令项目准备用openai测试 安装命令 本地&#xff08;docker compose&#xff09;&#xff1a;使用 Docker Compose 在你的机器上于 5 分钟内运行 Langfuse。 # 获取最新的 Langfuse 仓库副本 git clone https://github.com/langfuse/langfuse.git cd langfuse# 运行 …

每天学一个 Linux 命令(35):dos2unix

每天学一个 Linux 命令(35):dos2unix 命令简介 dos2unix 是一个用于将 Windows/DOS 格式的文本文件转换为 Unix/Linux 格式的实用工具。它主要处理行尾符的转换(将 CRLF 转换为 LF),同时也能处理编码问题和字符集转换。这个命令在跨平台文件共享、代码迁移和系统管理场…

第6章 Python 基本数据类型详解(int, float, bool, str)细节补充

文章目录 Python 基本数据类型深入解析(int, float, bool, str)一、整型(int)的底层机制二、浮点型(float)的陷阱与解决方案三、布尔型(bool)的底层本质四、字符串(str)的不可变性与优化五、类型间的隐式转换与陷阱六、性能优化与工具总结:关键细节与最佳实践Python…

19. LangChain安全与伦理:如何避免模型“幻觉“与数据泄露?

引言&#xff1a;当AI成为企业"数字员工"时的责任边界 2025年某金融机构因AI客服泄露用户信用卡信息被罚款2300万美元。本文将基于LangChain的安全架构与Deepseek-R1的合规实践&#xff0c;揭示如何构建既强大又安全的AI系统。 一、AI安全风险矩阵 1.1 2025年最新威…

Java快速上手之实验六

1. 编写ItemEventDemo.java&#xff0c;当选中或取消选中单选钮、复选钮和列表框时显示所选的结果。 2&#xff0e;编写GUIExample.java&#xff0c;当选中或取消选中单选钮、复选钮时在标签中显示相应结果。 import javax.swing.*; import java.awt.*; import java.awt.event.…

QT6 源(72):阅读与注释单选框这个类型的按钮 QRadioButton,及各种属性验证,

&#xff08;1&#xff09;按钮间的互斥&#xff1a; &#xff08;2&#xff09;源码来自于头文件 qradiobutton . h &#xff1a; #ifndef QRADIOBUTTON_H #define QRADIOBUTTON_H#include <QtWidgets/qtwidgetsglobal.h> #include <QtWidgets/qabstractbutton.h>…

【算法滑动窗口】 将x减到0的最小操作数

将x减到0的最小操作数 个人总结的八步归纳AI的归纳**8步归纳法&#xff08;极简直白版&#xff09;**1. 问题本质2. 问题特征3. 切入点4. 解决流程5. 每步目标与操作6. 注意事项7. 最终目标8. 整体总结 代码对照&#xff08;逐行解析&#xff09;举个栗子&#x1f330;**一句话…

RISC-V GPU架构研究进展:在深度学习推理场景的可行性验证

一、新型算力架构的突围战 在英伟达CUDA生态主导的GPU市场中&#xff0c;RISC-V架构正以‌开源基因‌和‌模块化设计‌开辟新赛道。当前主流GPU架构面临两大痛点&#xff1a; 指令集封闭性‌&#xff1a;NVIDIA的SASS指令集与AMD的GCN/RDNA架构均采用私有指令编码&#xff0c…

LVGL -滑动条

1 滑动条 LVGL 的滑动条(Slider)是一个非常有用的控件,允许用户通过拖动滑块或点击滑条来选择一个值。 1.1 基本定义 滑动条允许用户在一个预定义的数值范围内选择一个特定的值。它通常由一个轨道(track)和一个滑块(thumb)组成。用户可以通过点击或拖动滑块来调整数值。…

ROS2学习笔记|Python实现订阅消息并朗读的详细步骤

本教程将详细介绍如何使用 ROS 2 实现一个节点订阅另一个节点发布的消息&#xff0c;并将接收到的消息通过 espeakng 库进行朗读的完整流程。以下步骤假设你已经安装好了 ROS 2 环境&#xff08;以 ROS 2 Humble 为例&#xff09;&#xff0c;并熟悉基本的 Linux 操作。 注意&…

WPF封装常用的TCP、串口、Modbus、MQTT、Webapi、PLC通讯工具类

WPF封装常用通讯工具类 下面我将为您封装常用的TCP、串口、Modbus、MQTT、WebAPI和PLC通讯工具类,适用于WPF应用程序开发。 一、TCP通讯工具类 using System; using System.Net.Sockets; using System.Text; using System.Threading.Tasks;public class TcpClientHelper : …

npm pnpm yarn 设置国内镜像

国内镜像 常用的国内镜像&#xff1a; 淘宝镜像 https://registry.npmmirror.com 腾讯云镜像​​ https://mirrors.cloud.tencent.com/npm/ 华为云镜像​​ https://repo.huaweicloud.com/repository/npm/ CNPM&#xff08;阿里系&#xff09; ​​ https://r.cnpmjs.org/ 清华…