وقتی پیامک OTP کار نمیکند: احراز هویت با تک زنگ!

احراز هویت با تک زنگ

در روزهای اخیر، علاوه بر قطعی اینترنت، شاهد اختلال و قطعی در خطوط تلفن، پیام‌رسان‌ها و به‌طور کلی هر پلتفرمی بوده‌ایم که امکان تبادل پیام را فراهم می‌کند. یکی از پیامدهای مستقیم این شرایط، اختلال در ارسال پیامک‌های تجاری، به‌ویژه پیامک‌های OTP یا رمز یک‌بارمصرف ورود است؛ قابلیتی که امروزه در اغلب سرویس‌های آنلاین برای احراز هویت کاربران استفاده می‌شود.

در نخستین روز قطعی سراسری اینترنت، با کاربرانی مواجه شدیم که امکان ورود به حساب کاربری یا ثبت‌نام در پلتفرم گیت را نداشتند و ناچار بودیم به‌صورت دستی کد ورود را در اختیار آن‌ها قرار دهیم. بلافاصله به دنبال راه‌حلی جایگزین رفتیم و در روز دوم، یک روش عملی و مؤثر ارائه کردیم که به شکل جالبی این مشکل را برطرف کرد. این راه‌حل تا لحظه انتشار این مقاله، یعنی دوازدهمین روز قطعی اینترنت، همچنان فعال است و به‌خوبی کار می‌کند.

در همان زمان، پیش‌نویس این مقاله را آماده کردیم تا سایر وب‌سایت‌ها و سرویس‌هایی که به احراز هویت وابسته هستند نیز بتوانند از این روش استفاده کنند، اما به دلایلی انتشار آن به تعویق افتاد. با وجود اینکه در حال حاضر پیامک‌های OTP ظاهراً دوباره متصل شده‌اند، در عمل برای بخش قابل توجهی از کاربران همچنان کدهای تأیید ارسال نمی‌شود.

بنابراین اگر صاحب یک کسب‌وکار، اپلیکیشن یا سرویس آنلاین هستید که برای احراز هویت کاربران از پیامک OTP یا کد یک‌بارمصرف استفاده می‌کند، می‌توانید احراز هویت با «تک زنگ» را به‌عنوان یک راه‌حل جایگزین، کم‌هزینه و حتی در شرایط بحرانی به‌عنوان تنها راه‌حل عملی در نظر بگیرید.

برای حل این مشکل در شرایطی که دسترسی به هیچ‌یک از روش‌های متداول احراز هویت مانند پیامک یا ایمیل وجود ندارد، از یک ایده ساده استفاده کردیم؛ ایده‌ای که علاوه بر پیاده‌سازی سریع، عملکرد قابل قبولی نیز دارد. کل فرآیند، از شکل‌گیری ایده تا اجرای نهایی، در کمتر از دو ساعت و آن هم بدون دسترسی به اینترنت انجام شد.

در این روش، کاربر در هر صفحه‌ای که نیاز به تأیید شماره تلفن دارد (مانند صفحه ثبت‌نام یا ورود به حساب کاربری)، کافی است به شماره‌ای که از سوی شما مشخص شده است یک تک زنگ بزند. سایر مراحل به‌صورت خودکار انجام می‌شود و این روش از نظر امنیتی نیز قابل اتکا است.

این راهکار تنها با یک سیم‌کارت معمولی و یک اپلیکیشن اندرویدی اجرا می‌شود و روی اغلب پلتفرم‌ها به‌راحتی قابل پیاده‌سازی است.

معماری کلی احراز هویت با تک‌زنگ (Missed Call)

معماری احراز هویت تک زنگ

در روش احراز هویت با تک‌زنگ، سرور نقش مرجع اعتبارسنجی نهایی را دارد و هیچ تصمیم امنیتی در سمت کلاینت گرفته نمی‌شود. اپلیکیشن اندرویدی صرفاً نقش جمع‌آوری داده تماس و ارسال آن به سرور را ایفا می‌کند.

به‌صورت خلاصه، جریان کار به شکل زیر است:

  1. سرور یک شماره تلفن مشخص (یا از یک Pool شماره) به کاربر نمایش می‌دهد

  2. کاربر از شماره موبایل خود یک تماس کوتاه (Missed Call) با آن شماره می‌گیرد

  3. اپلیکیشن اندرویدی تماس ورودی را تشخیص می‌دهد

  4. اپلیکیشن، گزارش تماس را به سرور ارسال می‌کند

  5. سرور گزارش را اعتبارسنجی کرده و شماره کاربر را «Verified» می‌کند

مسئولیت‌های سمت سرور

در سمت سرور، یک متد برای دریافت گزارش تماس در نظر گرفته می‌شود که شامل شماره تماس‌گیرنده و یک توکن رمزنگاری‌شده برای تأمین امنیت درخواست است.

همچنین متدی دیگر وجود دارد که بررسی می‌کند آیا شماره تلفن کاربر تأیید (Verify) شده است یا خیر.

البته این فرآیند را می‌توان به روش‌های مختلفی تحلیل و پیاده‌سازی کرد، اما در نهایت ساختار کلی همه آن‌ها مشابه خواهد بود.

به‌عنوان نمونه، متدی با نام call-report داریم که از سمت اپلیکیشن اندرویدی فراخوانی شده و گزارش تماس را ارسال می‌کند:

 POST /api/call-report
Content-Type: application/json

{
  "phone": "+989123456789",
  "token": "5f2a7b..."   // HMAC یا JWT
}

سرور پس از دریافت این گزارش، آن را اعتبارسنجی می‌کند و در صورتی که کاربری با این شماره تلفن درخواست ورود ثبت کرده باشد، در متدی دیگر که توسط کلاینت (اپلیکیشن یا مرورگر) فراخوانی می‌شود، کد ورود به کاربر ارائه شده یا کاربر به‌صورت خودکار به مرحله بعد هدایت می‌شود.

متد بررسی وضعیت تأیید شماره تلفن و نمونه خروجی آن می‌تواند به شکل زیر باشد. در این مرحله، کاربر در سمت فرانت‌اند می‌تواند با کلیک روی دکمه‌ای مانند «بررسی تماس» یا «تماس گرفتم» وضعیت احراز هویت خود را مشاهده کند. همچنین می‌توان مانند کاری که ما انجام دادیم، این بررسی را هر چند ثانیه یک‌بار به‌صورت خودکار انجام داد تا بلافاصله پس از تأیید، کاربر وارد مرحله بعد شود.

 GET /api/phone-verified?phone=%2B989123456789
Content-Type: application/json

{
  "code": "25415"
}

 

مسئولیت های سمت اپلیکیشن:

در این بخش به یک اپلیکیشن نیاز داریم که قابلیت‌های زیر را داشته باشد:

  • دریافت گزارش تماس ورودی.
  • ارسال شماره تماس‌گیرنده به همراه توکن امنیتی به متد call-report (ترجیحاً با متد POST).
  • اجرای مداوم در پس‌زمینه و انتظار برای تماس‌های بعدی، بدون بسته شدن اپلیکیشن.

پیاده‌سازی اپلیکیشن اختصاصی:

پیاده‌سازی یک اپلیکیشن برای انجام این وظایف پیچیدگی زیادی ندارد و با چند خط کد جاوا یا کاتلین قابل انجام است. ما در این پروژه از یک راه‌حل جایگزین استفاده کردیم، اما در صورت نیاز به توسعه اپلیکیشن اختصاصی، می‌توانید از الگوهای زیر برای راهنمایی استفاده کنید:

مجوزهای موردنیاز (AndroidManifest.xml)

<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

<application
    ...>

    <receiver
        android:name=".CallReceiver"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.PHONE_STATE"/>
        </intent-filter>
    </receiver>

    <service
        android:name=".CallForegroundService"
        android:exported="false"
        android:foregroundServiceType="phoneCall"/>

</application>

دریافت تماس ورودی (BroadcastReceiver)

public class CallReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

        if (TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(intent.getAction())) {

            String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);

            if (TelephonyManager.EXTRA_STATE_RINGING.equals(state)) {
                String incomingNumber =
                        intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);

                Intent serviceIntent =
                        new Intent(context, CallForegroundService.class);
                serviceIntent.putExtra("phone", incomingNumber);

                ContextCompat.startForegroundService(context, serviceIntent);
            }
        }
    }
}

Foreground Service (اجرای دائمی)

public class CallForegroundService extends Service {

    private static final String CHANNEL_ID = "call_channel";

    @Override
    public void onCreate() {
        super.onCreate();
        createNotificationChannel();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

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

        startForeground(1, getNotification());

        if (phone != null) {
            sendCallReport(phone);
        }

        return START_STICKY;
    }

    private Notification getNotification() {
        return new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("Call Monitor Active")
                .setContentText("Monitoring incoming calls")
                .setSmallIcon(android.R.drawable.sym_call_incoming)
                .setOngoing(true)
                .build();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel =
                    new NotificationChannel(
                            CHANNEL_ID,
                            "Call Monitor",
                            NotificationManager.IMPORTANCE_LOW
                    );

            NotificationManager manager =
                    getSystemService(NotificationManager.class);
            manager.createNotificationChannel(channel);
        }
    }
}

ارسال شماره تماس با POST + توکن امنیتی

private void sendCallReport(String phone) {

    new Thread(() -> {
        try {
            URL url = new URL("https://example.com/call-report");
            HttpURLConnection conn =
                    (HttpURLConnection) url.openConnection();

            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type", "application/json");
            conn.setDoOutput(true);

            JSONObject json = new JSONObject();
            json.put("phone", phone);
            json.put("token", "SECURE_TOKEN_123");

            OutputStream os = conn.getOutputStream();
            os.write(json.toString().getBytes(StandardCharsets.UTF_8));
            os.close();

            conn.getResponseCode();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }).start();
}

5. درخواست Runtime Permission (ضروری)

در Activity اصلی:

ActivityCompat.requestPermissions(
    this,
    new String[]{
        Manifest.permission.READ_PHONE_STATE,
        Manifest.permission.READ_CALL_LOG
    },
    1001
);

این کدها صرفاً جهت نمونه و درک بهتر موضوع ارائه شده‌اند و برای پیاده‌سازی عملی این اپلیکیشن، حتماً از یک فرد متخصص کمک بگیرید.

 

راه‌حل پیشنهادی: استفاده از اپلیکیشن Automate

اپلیکیشن Automate یک ابزار بسیار کاربردی است که به کمک آن می‌توانید بدون نیاز به کدنویسی، فرآیندهای مختلف را به‌صورت خودکار اجرا کنید.

این اپلیکیشن از طریق گوگل‌پلی قابل دریافت است و در شرایط قطعی اینترنت نیز توانستیم آن را از منابع داخلی دانلود کنیم.

از اینجا می‌توانید آن را دریافت کنید:

دانلود اپلیکیشن Automate

کار با این اپلیکیشن نسبتاً ساده است. پس از نصب، دسترسی‌های مورد نیاز را به آن بدهید و سپس یک Workflow جدید ایجاد کنید و تنظیمات آن را به‌گونه‌ای انجام دهید که به تماس‌های ورودی دسترسی داشته باشد و به‌صورت دائم در حال اجرا باشد.

مراحل پیشنهادی ما برای پیاده‌سازی این Workflow به شرح زیر است:

  • افزودن بلاک Flow beginning به‌عنوان نقطه شروع ورکفلو.
  • افزودن بلاک When ringing in-call برای انتظار دریافت تماس ورودی.
  • افزودن بلاک End call برای قطع تماس (در صورت پشتیبانی دستگاه).
  • افزودن بلاک HTTP request برای ارسال اطلاعات تماس به API سمت سرور.
  • افزودن بلاک Catch failure برای مدیریت خطاها و جلوگیری از متوقف شدن ورکفلو.
  • بلاک اختیاری: Show notification برای نمایش نتیجه در قالب نوتیفیکیشن.

اتصالات بین بلاک‌ها را با دقت تنظیم کنید تا ورکفلو عملکرد صحیحی داشته باشد. در نهایت، ورکفلو را اجرا (Start) کنید.

نمای ورکفلوی اولیه‌ای که ما طراحی کرده‌ایم و به‌خوبی کار می‌کند:

 

استفاده از احراز هویت با تک زنگ در مقیاس بزرگ

در صورتی که تعداد تماس‌های ورودی زیاد باشد، ممکن است بخشی از تماس‌ها از دست برود یا کاربران با پیام اشغال بودن خط مواجه شوند. برای حل این مشکل، می‌توانید به‌جای یک دستگاه اندرویدی، از چند دستگاه و چند شماره تلفن استفاده کنید تا تماس‌ها بین آن‌ها توزیع شده و فرآیند احراز هویت روان‌تر انجام شود.

 

سخن پایانی

در صورت تصمیم به اجرای این روش، حتماً ملاحظات امنیتی از جمله یک‌بارمصرف بودن کد یا توکن تولیدشده را به‌دقت رعایت کنید.

این مقاله با هدف معرفی این روش احراز هویت منتشر شده است و یک آموزش گام‌به‌گام کامل محسوب نمی‌شود. در صورت وجود سؤال، می‌توانید آن را در بخش نظرات مطرح کنید. همچنین اگر ایده یا تکنیک خلاقانه‌ای در این زمینه دارید، خوشحال می‌شویم آن را با ما و سایر کاربران به اشتراک بگذارید.

 

برای ثبت دیدگاه وارد حساب کاربری خود شوید.