درستی‌سنجی فرم گذرواژه یکبارمصرف پیامکی

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

ترکیب نشانه‌گذاری معنایی <form>،‏ <input autocomplete="one-time-code" inputmode="numeric">، قالب‌بندی پیامک محدود به مبدأ (@domain #code)، و WebOTP API‏ (OTPCredential) یک جریان درستی‌سنجی لایه‌ای و به‌تدریج بهبودیافته ایجاد می‌کند:

  1. استخراج و ارسال خودکار: مرورگرهایی که از WebOTP API پشتیبانی می‌کنند، مبدأ را با خط @domain #code در پیامک درستی‌سنجی می‌کنند، <input> را تکمیل می‌کنند، و درستی‌سنجی را با یک تک‌ضرب فعال می‌کنند.
  2. تکمیل خودکار صفحه‌کلید محدود به مبدأ: مرورگرها و سیستم‌عامل‌هایی که از autocomplete="one-time-code" پشتیبانی می‌کنند (مانند Safari در iOS،‏ iPadOS، و macOS) پیام @domain #code را تجزیه می‌کنند و کد را در نوار تکمیل خودکار صفحه‌کلید فقط زمانی پیشنهاد می‌دهند که دامنه مطابقت داشته باشد.
  3. ورود دستی بدون اصطکاک: کاربرانی که کدی را از دستگاه دیگری وارد می‌کنند، صفحه‌کلید مجازی عددی را ازطریق inputmode="numeric" دریافت می‌کنند، بدون اینکه با خطرات قابلیت استفاده <input type="number"> مواجه شوند.

ملاحظات امنیتی

گذرواژه یک‌بارمصرف پیامکی مالکیت شماره تلفن را درستی‌سنجی می‌کند و درستی‌سنجی گام‌به‌گام راحتی ارائه می‌دهد، اما انتقال پیامک و شماره تلفن‌ها ممکن است درمعرض تعویض سیم‌کارت، بازیافت شماره، و رهگیری قرار بگیرند. همیشه تکمیل خودکار سمت کارخواه و WebOTP API را با قالب پیامک @domain #code محدود به مبدأ جفت کنید تا مرورگر و سیستم‌عامل قبل‌از پیشنهاد یا ارائه کد، مطابقت دامنه هدف را تأیید کنند. برای اصالت‌سنجی اصلی مقاوم دربرابر رمزگیری، گذرکلیدها را به کدهای مبتنی بر پیامک ترجیح دهید (به جهت‌گیری گذرکلیدها مراجعه کنید).

نشانه‌گذاری فرم معنایی

کنترل درستی‌سنجی را در عنصر معنایی <form> با method="POST" و <button type="submit"> صریح قرار دهید. همیشه از یک عنصر <input> استفاده کنید و ارقام را در چند چارگوش تقسیم نکنید.

  • استفاده کردن از type="text" با inputmode="numeric": هرگز از type="number" برای کدهای OTP استفاده نکنید. گذرنویسه یک‌بارمصرف رشته‌ای شناساننده است که ممکن است با صفر شروع شود (مثل 012345) و مقدار قابل‌شمارش نیست. استفاده از type="number" دکمه‌های چرخنده افزایشی/کاهشی اضافه می‌کند، می‌تواند صفرهای ابتدایی را حذف کند، و امکان تغییر تصادفی مقدار با چرخ پیمایش را فراهم می‌کند. همچنین از type="tel" که راه‌حل قدیمی برای شماره تلفن‌های کامل است استفاده نکنید. type="text" را با inputmode="numeric" جفت کنید تا صفحه‌کلید مجازی فقط عددی در دستگاه‌های همراه ارائه شود.
  • افزودن autocomplete="one-time-code": به مرورگرها و سیستم‌عامل‌ها دستور می‌دهد کدهای درستی‌سنجی پیامکی ورودی را در نوار تکمیل خودکار پیشنهاد دهند، وقتی <input> در کانون توجه است.
  • نکات آموزشی را بالای <input> قرار دهید: بااستفاده از for و id، یک <label> نمایان را مرتبط کنید و هرگونه دستورالعمل قالب‌بندی (پیوندشده ازطریق aria-describedby) را به‌صورت دیداری بالای <input> قرار دهید تا منوهای بالاپر تکمیل خودکار و صفحه‌کلیدهای مجازی تلفن همراه هرگز دستورالعمل‌ها را نپوشانند.
  • رقم‌ها را در چند عنصر <input> تقسیم نکنید: الگوهای گذرواژه یک‌بارمصرف چندجعبه‌ای (شش ورودی maxlength="1" جداگانه) تکمیل خودکار autocomplete="one-time-code" بومی را در بسیاری از مرورگرها خراب می‌کند، مانع از کپی-جای‌گذاری می‌شود، و پیمایش صفحه‌خوان را پیچیده می‌کند. اگر طراحی دیداری بخش‌بندی‌شده لازم است، یک <input> را با letter-spacing و قلم تک‌فاصله سبک‌بندی کنید.
<form id="otp-form" action="/verify-otp" method="POST">
  <div class="form-group">
    <label for="otp-input">Verification code</label>
    <!-- Place the hint above the input so autofill popovers and mobile keyboards do not obscure it -->
    <p id="otp-format-hint" class="field-hint">
      Enter the 6-digit code sent to your phone.
    </p>
    <!-- MANDATORY: Use a single input with type="text", inputmode="numeric", and autocomplete="one-time-code" -->
    <input
      type="text"
      id="otp-input"
      name="otp"
      inputmode="numeric"
      autocomplete="one-time-code"
      pattern="\d{6}"
      maxlength="6"
      aria-describedby="otp-format-hint"
      required>
  </div>
  <button type="submit">Verify code</button>
</form>
/* Optional: Style a single input to visually space digits without splitting the DOM element */
#otp-input {
  font-family: ui-monospace, monospace;
  font-size: 1.25rem; /* Example value: must be at least 1rem (16px) to prevent mobile browser auto-zoom */
  letter-spacing: 0.35em;
  min-height: 48px;
  padding: 0.5rem 0.75rem;
  box-sizing: border-box;
}

قالب پیامک محدود به مبدأ

برای اینکه هم WebOTP API و هم تکمیل خودکار autocomplete="one-time-code" محدود به دامنه Safari درستی‌سنجی کنند که کد متعلق به وب‌سایت شما است، سرویس پیامک باطن شما باید خط آخر پیامک را با دامنه مبدأ با پیشوند @ و گذرواژه یک‌بارمصرف با پیشوند # قالب‌بندی کند:

Your verification code is 123456.

@example.com #123456

قوانین قالب‌بندی زیر را بر پیامک خروجی اعمال کنید:

  • ابتدا نوشتار قابل‌خواندن برای انسان را اضافه کنید: همیشه پیامک را با یک جمله واضح و قابل‌خواندن برای انسان که حاوی کد است شروع کنید تا کاربرانی که آن را به‌صورت دستی در دستگاه دیگری تایپ می‌کنند بتوانند به‌راحتی آن را بخوانند.
  • @<domain> #<otp> را در آخرین خط قرار دهید: نویسه @ باید اولین نویسه خط آخر باشد و هیچ خط یا نوشتار امضایی بعداز آن وجود نداشته باشد.
  • فقط از دامنه میزبان استفاده کنید: طرح‌واره نشانی وب (https:// یا http://)، درگاه (:8080)، یا مسیر (/verify) را در کد @domain وارد نکنید.
  • جدا کردن توکن‌ها با یک فاصله: دقیقاً یک فاصله بین @<domain> و #<otp> قرار دهید و هرگز فاصله خالی در داخل دامنه یا بین # و کد قرار ندهید.

ادغام کردن WebOTP API در جریان درستی‌سنجی

WebOTP API (OTPCredential) به «بافت ایمن» (https:// در تولید، یا http://localhost درطول توسعه محلی) نیاز دارد. به‌جای جای‌گذاری کردن یک اسکریپت DOMContentLoaded قطع‌شده که DOM را پرسمان می‌کند و form.submit() را فراخوانی می‌کند، navigator.credentials.get() را مستقیماً در چرخه حیات و کنترل‌کننده ارسال واسط کاربر درستی‌سنجی‌تان ادغام کنید:

  • چرخه حیات درخواست را به مرحله فعال گذرواژه یک‌بارمصرف گره بزنید: وقتی ورودی درستی‌سنجی پرداز و فعال می‌شود، گوش دادن به گذرواژه یک‌بارمصرف را شروع کنید (برای مثال، وقتی مرحله گذرواژه یک‌بارمصرف پس‌از درخواست کاربر برای کد پیامکی سوار می‌شود، و دوباره هروقت کاربر روی دکمه «ارسال مجدد کد» کلیک می‌کند).
  • مدیریت AbortController در همه مسیرهای خروج: AbortSignal (signal: ac.signal) را به navigator.credentials.get() ارسال کنید. هرزمان که خواستید با ac.abort() تماس بگیرید:
    • کاربر فرم را به‌صورت دستی ارسال می‌کند (یا کدی را که ازطریق autocomplete="one-time-code" تکمیل شده است ارسال می‌کند).
    • کد پیامکی جدیدی درخواست می‌شود («ارسال مجدد کد»)، که کنترل‌کننده قبلی را قبل‌از ایجاد کنترل‌کننده جدید لغو می‌کند.
    • عنصر، کادر گفتگوی، یا مسیر درستی‌سنجی لغو نصب شود.
  • راه‌انداز گرداننده ارسال برنامه (form.requestSubmit()): وقتی navigator.credentials.get() با otp حل شد، otpInput.value = otp.code را تنظیم کنید و با form.requestSubmit() تماس بگیرید (یا مستقیماً تابع درستی‌سنجی را فراخوانی کنید). form.submit() را فراخوانی نکنید، زیرا form.submit() شنوندگان رویداد submit (گرداننده‌های event.preventDefault() / fetch()) را دور می‌زند و از اعتبارسنجی محدودیت بومی رد می‌شود.
const otpForm = document.getElementById('otp-form');
const otpInput = document.getElementById('otp-input');

let otpAbortController = null;

function stopSmsOtpListener() {
  if (otpAbortController) {
    otpAbortController.abort();
    otpAbortController = null;
  }
}

async function startSmsOtpListener() {
  // Feature-detect WebOTP support
  if (!otpInput || !('OTPCredential' in window)) {
    return;
  }

  // Cancel any previous pending WebOTP request (e.g., when re-sending an SMS code)
  stopSmsOtpListener();
  const ac = new AbortController();
  otpAbortController = ac;

  try {
    const otp = await navigator.credentials.get({
      otp: { transport: ['sms'] },
      signal: ac.signal,
    });

    if (!otp) return;
    otpInput.value = otp.code;

    // Trigger standard form submission so submit event listeners and validation run
    if (otpForm) {
      otpForm.requestSubmit();
    }
  } catch (err) {
    // Expected when aborted by manual submission, resend, unmount, or user dismissal
    if (err && err.name === 'AbortError') {
      return;
    }
    console.error('WebOTP verification error:', err);
  }
}

otpForm.addEventListener('submit', async (event) => {
  // MANDATORY: Abort any in-flight WebOTP request when the form is submitted
  stopSmsOtpListener();

  // Application-specific submission logic (e.g., AJAX fetch or native POST)
  event.preventDefault();
  const formData = new FormData(otpForm);
  // await fetch(otpForm.action, { method: 'POST', body: formData });
});

// Start listening when the OTP verification step becomes active
startSmsOtpListener();

درستی‌سنجی iframe میان‌مبدئی

دریافت گذرواژه یک‌بارمصرف پیامکی در <iframe> میان‌مبدئی به‌طور پیش‌فرض محدود شده است و عمدتاً برای جریان‌های تأیید پرداخت جاسازی‌شده (مانند درستی‌سنجی 3D Secure که در آن shop.example چالش صادرکننده را از bank.example جاسازی می‌کند) استفاده می‌شود. پشتیبانی از WebOTP در <iframe> میان‌مبدئی به سه پیکربندی هماهنگ نیاز دارد:

  1. هر دو مبدأ را در خط آخر پیامک مقید کنید: ابتدا دامنه جاسازی سطح بالا (@shop.example) را فهرست کنید، سپس #<otp>، سپس یک فاصله، و سپس دامنه iframe جاسازی‌شده (@bank.example):

    Your verification code is 123456.
    
    @shop.example #123456 @bank.example
    
  2. ارسال سرایند پاسخ HTTP Permissions-Policy: otp-credentials را به مبدأ میان‌مبدئی iframe در سرایندهای HTTP سند اصلی واگذار کنید:

    Permissions-Policy: otp-credentials=(self "https://bank.example")
    
  3. allow="otp-credentials" را به عنصر <iframe> اضافه کنید: خط‌مشی اجازه را به‌طور صریح در برچسب <iframe> در صفحه جاسازی‌شده اعطا کنید و یک مشخصه توصیفی title برای صفحه‌خوان‌ها اضافه کنید:

<iframe
  src="https://bank.example/verify"
  allow="otp-credentials"
  title="Payment verification">
</iframe>

درون سند <iframe> که توسط https://bank.example ارائه شده است، همان نشان‌گذاری <form> و چرخه حیات درستی‌سنجی navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal }) را که در بالا نشان داده شده است اجرا کنید.

راهبردهای جایگزین

جایگزین WebOTP API

اگر مرورگرهای هدف شما از WebOTP API (OTPCredential) پشتیبانی نمی‌کنند، شبیه‌ساز مرورگر جاوا اسکریپت شخص ثالث را بار نکنید، زیرا خواندن پیامک‌های ورودی به ادغام در سطح سیستم‌عامل نیاز دارد. درعوض، به ارتقای پیش‌رو سه‌سطحی که در نشانه‌گذاری <input> تعبیه شده است تکیه کنید:

  • تجربه ارتقای پیش‌رو: از تماس navigator.credentials.get({ otp: { transport: ['sms'] } }) در پشت if ('OTPCredential' in window) محافظت کنید. وقتی OTPCredential دردسترس نیست:
    1. مرورگرهایی که از autocomplete="one-time-code" پشتیبانی می‌کنند (مانند Safari در iOS 14+،‏ iPadOS، و macOS با iCloud SMS relay) پیامک @domain #code را تجزیه می‌کنند و وقتی <input> در کانون توجه قرار می‌گیرد، کد را در نوار تکمیل خودکار صفحه‌کلید سیستم ارائه می‌دهند.
    2. همه مرورگرهای دیگر به ورودی دستی در <input type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" required> تکی برمی‌گردند، صفحه‌کلید عددی را در دستگاه‌های همراه نمایش می‌دهند، و ازطریق <button type="submit"> ارسال می‌کنند.

جایگزین «خط‌مشی اجازه‌ها»

اگر مرورگر از واگذاری otp-credentials ازطریق Permissions-Policy و <iframe allow="otp-credentials"> پشتیبانی نکند، درستی‌سنجی جاسازی‌شده <form> در <iframe> میان‌مبدئی به‌طور مناسب به پیشنهادهای صفحه‌کلید autocomplete="one-time-code" و ورودی عددی دستی تنزل می‌یابد بدون اینکه استثناهای مدیریت‌نشده ایجاد کند.

پشتیبانی از Inputmode و AbortController

هم مشخصه HTML inputmode و هم AbortController (AbortSignal) در مرورگرهای نو پشتیبانی می‌شوند و به شبیه‌ساز مرورگر یا کد جایگزین نیاز ندارند.