Dostarczanie jednorazowego kodu dostępu przez SMS to powszechny sposób weryfikacji numeru telefonu, odzyskiwania konta, uwierzytelniania wielopoziomowego i potwierdzania płatności. Przełączanie się między przeglądarką a aplikacją do obsługi wiadomości SMS w celu zapamiętania lub skopiowania i wklejenia wielocyfrowego kodu powoduje problemy dla użytkowników, zwiększa współczynnik porzuceń i naraża użytkowników na wyłudzanie informacji w sfałszowanych domenach.
Połączenie semantycznych znaczników <form>, <input autocomplete="one-time-code" inputmode="numeric">, formatowania SMS-ów powiązanego z pochodzeniem (@domain #code) i interfejsu WebOTP API (OTPCredential) tworzy wielowarstwowy, proces weryfikacji z progresywnym ulepszaniem:
- Automatyczne wyodrębnianie i przesyłanie: przeglądarki obsługujące interfejs WebOTP API weryfikują pochodzenie na podstawie wiersza
@domain #codew SMS-ie, wypełniają pole<input>i uruchamiają weryfikację jednym kliknięciem. - Autouzupełnianie klawiatury powiązane z domeną: przeglądarki i systemy operacyjne obsługujące
autocomplete="one-time-code"(np. Safari na iOS, iPadOS i macOS) analizują wiadomość@domain #codei sugerują kod na pasku autouzupełniania klawiatury tylko wtedy, gdy domena jest zgodna. - Bezproblemowe ręczne wpisywanie: użytkownicy wpisujący kod z innego urządzenia otrzymują numeryczną klawiaturę wirtualną w
inputmode="numeric", co pozwala uniknąć problemów z użytecznością, które występują w<input type="number">.
Bezpieczeństwo
SMS z hasłem jednorazowym potwierdza posiadanie numeru telefonu i zapewnia wygodną weryfikację, ale SMS-y i numery telefonów mogą być narażone na zamianę karty SIM, ponowne wykorzystanie numeru i przechwycenie. Zawsze łącz autouzupełnianie po stronie klienta i interfejs WebOTP API z formatem SMS-ów @domain #code powiązanych z domeną, aby przeglądarka i system operacyjny mogły sprawdzić, czy domena docelowa jest zgodna, zanim zasugerują lub dostarczą kod. W przypadku podstawowego uwierzytelniania odpornego na phishing preferuj klucze dostępu zamiast kodów SMS (patrz Wprowadzenie do kluczy dostępu).
Semantyczne znaczniki formularzy
Umieść element sterujący weryfikacją w semantycznym elemencie <form> z atrybutem method="POST" i wyraźnym atrybutem <button type="submit">. Zawsze używaj jednego elementu <input>, zamiast dzielić cyfry na kilka pól.
- Używanie
type="text"zinputmode="numeric": nigdy nie używajtype="number"do kodów haseł jednorazowych. Jednorazowy kod dostępu to ciąg znaków identyfikatora, który może zawierać zera na początku (np.012345), a nie policzalną ilość. Użycietype="number"powoduje dodanie przycisków zwiększania/zmniejszania, może usuwać zera wiodące i umożliwia przypadkowe zmiany wartości kółkiem przewijania. Unikaj teżtype="tel", który jest starszym obejściem przeznaczonym dla pełnych numerów telefonów. Połącztype="text"zinputmode="numeric", aby na urządzeniach mobilnych wyświetlać klawiaturę wirtualną zawierającą tylko cyfry. - Dodaj
autocomplete="one-time-code": instruuje przeglądarki i systemy operacyjne, aby sugerowały przychodzące kody weryfikacyjne z SMS-ów na pasku autouzupełniania, gdy element<input>jest aktywny. - Umieść wskazówki nad elementem
<input>: powiąż widoczny element<label>za pomocą atrybutówforiidoraz umieść instrukcje dotyczące formatu (połączone za pomocą atrybutuaria-describedby) wizualnie nad elementem<input>, aby wyskakujące okienka autouzupełniania i klawiatury wirtualne na urządzeniach mobilnych nigdy nie zasłaniały instrukcji. - Nie dziel cyfr między wiele elementów
<input>: wzorce haseł jednorazowych z wieloma polami (6 osobnych pól wejściowychmaxlength="1") uniemożliwiają natywne autouzupełnianieautocomplete="one-time-code"w wielu przeglądarkach, utrudniają kopiowanie i wklejanie oraz komplikują nawigację za pomocą czytnika ekranu. Jeśli wymagany jest podzielony projekt wizualny, zastosuj do pojedynczego elementu<input>stylletter-spacingi czcionkę o stałej szerokości znaków.
<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;
}
Format SMS-a powiązanego z miejscem pochodzenia
Aby zarówno interfejs WebOTP API, jak i funkcja autouzupełniania powiązana z domeną autocomplete="one-time-code" w Safari mogły zweryfikować, czy kod należy do Twojej witryny, usługa SMS w backendzie musi sformatować ostatni wiersz SMS-a, dodając domenę źródłową poprzedzoną znakiem @ i hasło jednorazowe poprzedzone znakiem #:
Your verification code is 123456.
@example.com #123456
Zastosuj te reguły formatowania do SMS-a wychodzącego:
- Najpierw umieść tekst zrozumiały dla człowieka: zawsze zaczynaj SMS-a od zrozumiałego dla człowieka zdania zawierającego kod, aby użytkownicy, którzy wpisują go ręcznie na innym urządzeniu, mogli go łatwo odczytać.
- Umieść
@<domain> #<otp>w ostatnim wierszu: znak@musi być pierwszym znakiem ostatniego wiersza, po którym nie może być żadnych dodatkowych wierszy ani tekstu podpisu. - Używaj tylko domeny hostowanej: nie uwzględniaj schematu URI adresu URL (
https://lubhttp://), portu (:8080) ani ścieżki (/verify) w tokenie@domain. - Oddzielaj tokeny pojedynczą spacją: umieść dokładnie jedną spację między
@<domain>i#<otp>. Nigdy nie umieszczaj spacji w domenie ani między#a kodem.
Integracja interfejsu WebOTP API z procesem weryfikacji
Interfejs WebOTP API (OTPCredential) wymaga bezpiecznego kontekstu (https:// w środowisku produkcyjnym lub http://localhost podczas programowania lokalnego). Zamiast wklejać niepowiązany skrypt DOMContentLoaded, który wysyła zapytania do DOM i wywołuje form.submit(), zintegruj navigator.credentials.get() bezpośrednio z cyklem życia i modułem obsługi przesyłania interfejsu weryfikacji:
- Powiąż cykl życia żądania z aktywnym krokiem hasła jednorazowego: zacznij nasłuchiwać hasła jednorazowego, gdy dane wejściowe weryfikacji zostaną wyrenderowane i będą aktywne (np. gdy krok hasła jednorazowego zostanie zamontowany po tym, jak użytkownik poprosi o kod SMS, i ponownie, gdy użytkownik kliknie przycisk „Wyślij kod ponownie”).
- Zarządzanie
AbortControllerwe wszystkich ścieżkach wyjścia: przekażAbortSignal(signal: ac.signal) donavigator.credentials.get(). Zadzwoń do firmyac.abort(), gdy:- Użytkownik ręcznie przesyła formularz (lub przesyła kod wypełniony przez
autocomplete="one-time-code"). - Zażądano nowego kodu SMS („Wyślij kod ponownie”), co spowodowało przerwanie poprzedniego kontrolera przed utworzeniem nowego.
- Komponent, okno lub ścieżka weryfikacji zostaną odmontowane.
- Użytkownik ręcznie przesyła formularz (lub przesyła kod wypełniony przez
- Uruchom moduł obsługi przesyłania aplikacji (
form.requestSubmit()): gdynavigator.credentials.get()zostanie rozwiązany zotp, ustawotpInput.value = otp.codei wywołajform.requestSubmit()(lub wywołaj bezpośrednio funkcję weryfikacji). NIE wywołuj metodyform.submit(), ponieważform.submit()omija detektory zdarzeńsubmit(moduły obsługievent.preventDefault()/fetch()) i pomija natywną weryfikację ograniczeń.
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();
Weryfikacja elementów iframe współdzielenia
Odbieranie SMS-ów z hasłem jednorazowym w ramach współdzielenia <iframe> jest domyślnie ograniczone i jest używane głównie w przypadku wbudowanych procesów potwierdzania płatności (takich jak weryfikacja 3D Secure, w której shop.example umieszcza wyzwanie wydawcy z bank.example). Obsługa WebOTP w ramach współdzielenia <iframe> wymaga 3 skoordynowanych konfiguracji:
Powiąż oba źródła w ostatnim wierszu SMS-a: najpierw podaj domenę najwyższego poziomu, w której umieszczony jest element iframe (
@shop.example), następnie#<otp>, a potem spację i domenę umieszczonego elementu iframe (@bank.example):Your verification code is 123456. @shop.example #123456 @bank.exampleWyślij nagłówek odpowiedzi HTTP
Permissions-Policy: Przekażotp-credentialsdo źródła elementu iframe współdzielenia w nagłówkach HTTP dokumentu nadrzędnego:Permissions-Policy: otp-credentials=(self "https://bank.example")Dodaj
allow="otp-credentials"do elementu<iframe>: wyraźnie przyznaj uprawnienia w tagu<iframe>na stronie umieszczania i uwzględnij opisowy atrybuttitledla czytników ekranu:
<iframe
src="https://bank.example/verify"
allow="otp-credentials"
title="Payment verification">
</iframe>
W dokumencie <iframe> udostępnianym przez https://bank.example uruchom te same znaczniki <form> i cykl życia weryfikacji navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal }), które zostały pokazane powyżej.
Strategie zastępcze
Zastępczy interfejs WebOTP API
Jeśli przeglądarki docelowe nie obsługują interfejsu WebOTP API (OTPCredential), nie wczytuj kodu polyfill JavaScript innej firmy, ponieważ odczytywanie przychodzących SMS-ów wymaga integracji na poziomie systemu operacyjnego. Zamiast tego użyj 3-poziomowego progresywnego ulepszania wbudowanego w znacznik <input>:
- Progresywne ulepszanie: zabezpiecz wywołanie
navigator.credentials.get({ otp: { transport: ['sms'] } })za pomocąif ('OTPCredential' in window). GdyOTPCredentialjest niedostępny:- Przeglądarki obsługujące
autocomplete="one-time-code"(np. Safari na iOS 14 lub nowszym, iPadOS i macOS z przekazywaniem SMS-ów w iCloud) analizują SMS-y@domain #codei wyświetlają kod na pasku autouzupełniania klawiatury systemowej, gdy<input>jest zaznaczony. - W pozostałych przeglądarkach użytkownik musi ręcznie wpisać kod w jednym polu
<input type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" required>, przy czym na urządzeniach mobilnych wyświetla się klawiatura numeryczna, a formularz przesyła się za pomocą przycisku<button type="submit">.
- Przeglądarki obsługujące
Zasady dotyczące uprawnień – kreacja zastępcza
Jeśli przeglądarka nie obsługuje przekazywania otp-credentials za pomocą Permissions-Policy i <iframe allow="otp-credentials">, wbudowana weryfikacja <form> w ramach współdzielenia <iframe> jest płynnie zastępowana sugestiami klawiatury autocomplete="one-time-code" i ręcznym wprowadzaniem cyfr bez zgłaszania nieobsługiwanych wyjątków.
Obsługa inputmode i AbortController
Zarówno atrybut HTML inputmode, jak i AbortController (AbortSignal) są obsługiwane w nowoczesnych przeglądarkach i nie wymagają kodu polyfill ani kodu zastępczego.