自动化手机短信验证
评论
收藏

自动化手机短信验证

经验分享
汤姆布利柏
2026-04-09 10:23·浏览量:193
汤姆布利柏
影刀中级开发者
发布于 2026-04-06 17:43更新于 2026-04-09 10:23193浏览

md整了好几天,tmd才发现小米手机要读取验证码光是读取短信权限还不够,还得再来一个读取通知类短信的权限才能读到验证码,真tm坑啊,然后华为荣耀OPPO等都有不同的要设置的东西。。。我是没这些手机只有个红米,所以接下来的大概率只适配小米手机。其他手机的话可以瞅瞅https://juejin.cn/post/7222897518501003319这位大佬的代码。一开始我读去通知,但是通知的短信在息屏下验证码是****,所以我改成了直接读取数据库。

核心思路就是:

短信写入数据库

ContentObserver 监听到变化

读取最新短信

WorkManager

发邮件

效果

发完邮件接下来就是在影刀调用"获取邮件",然后再正则获取验证码,这样就成功打通了自动化登录中的手机验证码的环节,当然有自己的服务器作为中转肯定比邮件好点,可惜我没钱。https://drive.google.com/file/d/1STTAWdgBFif5RLger4539REXxCGH1VMj/view?usp=drive_link软件要打开自启动,省电策略要无限制,开启无障碍,不要开省点模式。权限里读取短信与彩信打开,通知类短信打开。
目录结构:

代码如下:

无障碍MyAccessibilityService

package com.example.auth.Service

import android.accessibilityservice.AccessibilityService
import android.annotation.SuppressLint
import android.view.accessibility.AccessibilityEvent

@SuppressLint("AccessibilityPolicy")
class MyAccessibilityService : AccessibilityService() {
    override fun onAccessibilityEvent(event: AccessibilityEvent?) {}

    override fun onInterrupt() {}

}

短信获取:

package com.example.auth.sms

import android.content.Context
import android.database.ContentObserver
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.example.auth.worker.EmailWorker

class SmsObserver(
    private val context: Context
) : ContentObserver(Handler(Looper.getMainLooper())) {

    companion object {
        var lastMsg: String? = null
        var lastTime: Long = 0
    }

    override fun onChange(selfChange: Boolean) {
        super.onChange(selfChange)

        Log.d("SMS_TEST", "短信数据库变化")

        readLatestSms()
    }

    private fun readLatestSms() {
        try {
            val uri = Uri.parse("content://sms/inbox")

            val cursor = context.contentResolver.query(
                uri,
                arrayOf("address", "body", "date"),
                null,
                null,
                "date DESC LIMIT 1"
            )

            cursor?.use {
                if (it.moveToFirst()) {
                    val sender = it.getString(0) ?: ""
                    val body = it.getString(1) ?: ""

                    Log.d("SMS_TEST", "最新短信: $sender -> $body")

                    // ======================
                    // 🚫 去重(关键)
                    // ======================
                    val now = System.currentTimeMillis()
                    if (body == lastMsg && now - lastTime < 5000) {
                        Log.d("SMS_TEST", "重复短信(Observer)")
                        return
                    }

                    lastMsg = body
                    if (!body.contains("验证码")){
                        return
                    }
                    lastTime = now

                    // ======================
                    // 🚀 发给 WorkManager
                    // ======================
                    val data = Data.Builder()
                        .putString("sms_content", "来自:$sender 内容:$body")
                        .build()

                    val request = OneTimeWorkRequest.Builder(EmailWorker::class.java)
                        .setInputData(data)
                        .build()

                    WorkManager.getInstance(context).enqueue(request)
                }
            }

        } catch (e: Exception) {
            Log.e("SMS_TEST", "读取短信失败", e)
        }
    }
}

邮件任务EmailWorker

package com.example.auth.worker

import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.auth.EmailSender

/**
 * WorkManager 后台任务
 * 专门负责发送邮件
 */
class EmailWorker(
    context: Context,
    workerParams: WorkerParameters
) : Worker(context, workerParams) {

    override fun doWork(): Result {
        return try {
            val content = inputData.getString("sms_content") ?: ""

            val sp = applicationContext.getSharedPreferences("config", Context.MODE_PRIVATE)
            val email = sp.getString("email", "") ?: ""
            val authCode = sp.getString("authCode", "") ?: ""

            if (email.isBlank() || authCode.isBlank()) {
                Log.e("SMS_TEST", "邮箱或授权码为空")
                return Result.failure()
            }

            val emailSender = EmailSender(email, authCode)

            emailSender.sendEmail(
                email,
                "验证码通知",
                "短信内容: $content"
            )

            Result.success()

        } catch (e: Exception) {
            Log.e("SMS_TEST", "Worker发送失败", e)
            Result.retry()
        }
    }
}

短信发送:

package com.example.auth

import android.util.Log
import java.util.Properties
import javax.mail.Authenticator
import javax.mail.Message
import javax.mail.PasswordAuthentication
import javax.mail.Session
import javax.mail.Transport
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage

class EmailSender(
    private val fromEmail: String,
    private val authCode: String
) {

    fun sendEmail(toEmail: String, subject: String, content: String) {
        try {
            val props = Properties().apply {
                put("mail.smtp.host", "smtp.qq.com")
                put("mail.smtp.port", "465")
                put("mail.smtp.auth", "true")
                put("mail.smtp.ssl.enable", "true")
            }

            val session = Session.getInstance(props, object : Authenticator() {
                override fun getPasswordAuthentication(): PasswordAuthentication {
                    return PasswordAuthentication(fromEmail, authCode)
                }
            })

            val message = MimeMessage(session)
            message.setFrom(InternetAddress(fromEmail))
            message.setRecipients(
                Message.RecipientType.TO,
                InternetAddress.parse(toEmail)
            )
            message.subject = subject
            message.setText(content)

            Transport.send(message)

            Log.d("SMS_TEST", "邮件发送成功")

        } catch (e: Exception) {
            Log.e("SMS_TEST", "邮件发送失败", e)
            throw e  // ⚠️ 必须抛出,让 WorkManager 知道失败
        }
    }
}

入口:

package com.example.auth

import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.example.auth.sms.SmsObserver
import com.example.auth.ui.theme.AuthTheme
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    private val SMS_PERMISSION_CODE = 100
    private lateinit var sp: SharedPreferences
    private lateinit var smsObserver: SmsObserver
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        requestSmsPermission()
        // 初始化SP
        sp = getSharedPreferences("config", MODE_PRIVATE)
        // 注册短信监听
        smsObserver = SmsObserver(this)
        contentResolver.registerContentObserver(
            "content://sms".toUri(),
            true,
            smsObserver
        )
        setContent {
            MainUI()
        }
        Log.d("SMS_TEST", "App启动了")
        enableEdgeToEdge()
    }
    // 判断读取短信权限
    private fun hasSmsPermission(): Boolean {
        return checkSelfPermission(android.Manifest.permission.READ_SMS) ==
                PackageManager.PERMISSION_GRANTED
    }

    // 请求读取短信权限
    private fun requestSmsPermission() {
        if (!hasSmsPermission()) {
            requestPermissions(
                arrayOf(android.Manifest.permission.READ_SMS),
                SMS_PERMISSION_CODE
            )
        }
    }
    @Composable
    fun MainUI() {

        var email by remember { mutableStateOf(sp.getString("email", "") ?: "") }
        var authCode by remember { mutableStateOf(sp.getString("authCode", "") ?: "") }

        // 👇 Snackbar 状态
        val snackbarHostState = remember { SnackbarHostState() }
        val scope = rememberCoroutineScope()

        Scaffold(
            snackbarHost = { SnackbarHost(snackbarHostState) }
        ) { padding ->

            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(padding)
                    .padding(20.dp),
                verticalArrangement = Arrangement.Center
            ) {

                Text("邮件配置", style = MaterialTheme.typography.titleLarge)

                Spacer(modifier = Modifier.height(20.dp))

                OutlinedTextField(
                    value = email,
                    onValueChange = { email = it },
                    label = { Text("邮箱") },
                    modifier = Modifier.fillMaxWidth()
                )

                Spacer(modifier = Modifier.height(16.dp))

                OutlinedTextField(
                    value = authCode,
                    onValueChange = { authCode = it },
                    label = { Text("授权码") },
                    modifier = Modifier.fillMaxWidth()
                )

                Spacer(modifier = Modifier.height(24.dp))

                Button(
                    onClick = {
                        // 保存
                        sp.edit()
                            .putString("email", email)
                            .putString("authCode", authCode)
                            .apply()

                        // 👇 显示提示
                        scope.launch {
                            snackbarHostState.showSnackbar("保存成功 ✅")
                        }
                    },
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text("保存")
                }
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        contentResolver.unregisterContentObserver(smsObserver)
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    AuthTheme {
        Greeting("Android")
    }
}

配置AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-feature
        android:name="android.hardware.telephony"
        android:required="false" />
    <uses-permission android:name="android.permission.RECEIVE_SMS"
        tools:ignore="SmsAndCallLogPolicy" />
    <uses-permission android:name="android.permission.READ_SMS"
        tools:ignore="SmsAndCallLogPolicy" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Auth">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Auth">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".Service.MyAccessibilityService"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
            android:exported="true"
            tools:ignore="AccessibilityPolicy">

            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>

            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service_config" />
        </service>

    </application>

</manifest>

配置build.gradle.kts:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-feature
        android:name="android.hardware.telephony"
        android:required="false" />
    <uses-permission android:name="android.permission.RECEIVE_SMS"
        tools:ignore="SmsAndCallLogPolicy" />
    <uses-permission android:name="android.permission.READ_SMS"
        tools:ignore="SmsAndCallLogPolicy" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Auth">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Auth">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".Service.MyAccessibilityService"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
            android:exported="true"
            tools:ignore="AccessibilityPolicy">

            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>

            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service_config" />
        </service>

    </application>

</manifest>

邮箱授权码我就说个QQ的其他的自己找。打开QQ邮箱的设置->安全设置->生成授权码,这个授权码在影刀的“获取邮件”中也要用到

smsforworder

收藏2
全部评论1
最新
发布评论
评论