Gửi mật mã một lần (OTP) qua SMS là quy trình phổ biến để xác minh số điện thoại, khôi phục tài khoản, xác thực nâng cao và xác nhận thanh toán. Việc chuyển đổi giữa trình duyệt và ứng dụng nhắn tin SMS để ghi nhớ hoặc sao chép và dán mã gồm nhiều chữ số sẽ gây phiền hà cho người dùng, làm tăng tỷ lệ rớt mạng và khiến người dùng dễ bị lừa đảo trên các miền giả mạo.
Việc kết hợp mã đánh dấu <form> ngữ nghĩa, <input autocomplete="one-time-code" inputmode="numeric">, định dạng SMS liên kết với nguồn gốc (@domain #code) và WebOTP API (OTPCredential) sẽ tạo ra một quy trình xác minh nhiều lớp, được cải tiến tăng dần:
- Tự động trích xuất và gửi: Các trình duyệt hỗ trợ WebOTP API sẽ xác minh nguồn gốc dựa trên dòng
@domain #codetrong SMS, điền<input>và kích hoạt quy trình xác minh chỉ bằng một lần nhấn. - Tự động điền bằng bàn phím theo nguồn gốc: Các trình duyệt và hệ điều hành hỗ trợ
autocomplete="one-time-code"(chẳng hạn như Safari trên iOS, iPadOS và macOS) sẽ phân tích cú pháp thông báo@domain #codevà đề xuất mã trong thanh tự động điền bằng bàn phím chỉ khi miền khớp. - Nhập thủ công dễ dàng: Người dùng nhập mã từ một thiết bị khác sẽ nhận được bàn phím ảo dạng số thông qua
inputmode="numeric"mà không gặp phải các vấn đề về khả năng hữu dụng của<input type="number">.
Lưu ý về bảo mật
OTP qua SMS xác minh quyền sở hữu số điện thoại và cung cấp quy trình xác minh từng bước thuận tiện, nhưng việc truyền SMS và số điện thoại có thể bị hoán đổi SIM, tái sử dụng số và chặn. Luôn kết hợp tính năng tự động điền phía máy khách và WebOTP API với định dạng SMS @domain #code liên kết với nguồn gốc để trình duyệt và hệ điều hành xác minh rằng miền mục tiêu khớp trước khi đề xuất hoặc cung cấp mã. Đối với phương thức xác thực chính có khả năng chống lừa đảo, hãy ưu tiên khoá truy cập hơn mã dựa trên SMS (xem phần Hướng dẫn về khoá truy cập).
Mã đánh dấu biểu mẫu ngữ nghĩa
Gói thành phần điều khiển xác minh trong một phần tử <form> ngữ nghĩa có method="POST" và <button type="submit"> rõ ràng. Luôn sử dụng một phần tử <input> duy nhất thay vì chia các chữ số thành nhiều hộp.
- Sử dụng
type="text"vớiinputmode="numeric": Không bao giờ sử dụngtype="number"cho mã OTP. Mật mã một lần là một chuỗi giá trị nhận dạng có thể chứa các số 0 ở đầu (chẳng hạn như012345), chứ không phải là một số lượng có thể đếm được. Việc sử dụngtype="number"sẽ thêm các nút quay tăng/giảm, có thể loại bỏ các số 0 ở đầu và cho phép thay đổi giá trị con lăn chuột một cách ngẫu nhiên. Ngoài ra, hãy tránh dùngtype="tel", đây là giải pháp cũ dành cho số điện thoại đầy đủ. Ghép nốitype="text"vớiinputmode="numeric"để hiển thị bàn phím ảo chỉ có chữ số trên thiết bị di động. - Thêm
autocomplete="one-time-code": Hướng dẫn trình duyệt và hệ điều hành đề xuất mã xác minh qua SMS trong thanh tự động điền khi<input>được lấy tiêu điểm. - Đặt gợi ý hướng dẫn phía trên
<input>: Liên kết một<label>hiển thị bằngforvàid, đồng thời đặt mọi hướng dẫn định dạng (được liên kết quaaria-describedby) một cách trực quan phía trên<input>để cửa sổ bật lên tự động điền và bàn phím ảo trên thiết bị di động không bao giờ che khuất hướng dẫn. - Không chia chữ số thành nhiều phần tử
<input>: Các mẫu OTP nhiều hộp (6 dữ liệu đầu vàomaxlength="1"riêng biệt) sẽ làm hỏng tính năng tự động điềnautocomplete="one-time-code"gốc trong nhiều trình duyệt, cản trở việc sao chép và dán, đồng thời làm phức tạp việc điều hướng trình đọc màn hình. Nếu cần thiết kế hình ảnh phân đoạn, hãy tạo kiểu cho một<input>bằngletter-spacingvà phông chữ đơn cách.
<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;
}
Định dạng tin nhắn SMS liên kết với nguồn gốc
Để cả WebOTP API và tính năng tự động điền autocomplete="one-time-code" liên kết với miền của Safari xác minh rằng mã thuộc về trang web của bạn, dịch vụ SMS phụ trợ của bạn phải định dạng dòng cuối cùng của tin nhắn văn bản bằng miền gốc có tiền tố @ và OTP có tiền tố #:
Your verification code is 123456.
@example.com #123456
Thực thi các quy tắc định dạng sau đây đối với tin nhắn SMS đi:
- Đưa văn bản mà con người có thể đọc được vào trước: Luôn bắt đầu tin nhắn SMS bằng một câu rõ ràng mà con người có thể đọc được, trong đó có chứa mã để người dùng nhập mã theo cách thủ công trên một thiết bị khác có thể dễ dàng đọc được.
- Đặt
@<domain> #<otp>ở dòng cuối cùng: Ký tự@phải là ký tự đầu tiên của dòng cuối cùng, không có dòng nào sau đó hoặc văn bản chữ ký sau đó. - Chỉ sử dụng tên miền được lưu trữ: Đừng thêm lược đồ URL (
https://hoặchttp://), cổng (:8080) hoặc đường dẫn (/verify) vào mã thông báo@domain. - Phân tách các mã thông báo bằng một dấu cách: Đặt đúng một dấu cách giữa
@<domain>và#<otp>, đồng thời không bao giờ thêm dấu cách vào bên trong miền hoặc giữa#và mã.
Tích hợp WebOTP API vào quy trình xác minh
WebOTP API (OTPCredential) yêu cầu một Bối cảnh bảo mật (https:// trong môi trường thực tế hoặc http://localhost trong quá trình phát triển cục bộ). Thay vì dán một tập lệnh DOMContentLoaded không kết nối để truy vấn DOM và gọi form.submit(), hãy tích hợp trực tiếp navigator.credentials.get() vào vòng đời và trình xử lý gửi của giao diện người dùng xác minh:
- Gắn vòng đời yêu cầu với bước OTP đang hoạt động: Bắt đầu theo dõi OTP khi dữ liệu xác minh đầu vào được hiển thị và hoạt động (ví dụ: khi bước OTP được kích hoạt sau khi người dùng yêu cầu mã SMS và mỗi khi người dùng nhấp vào nút "Gửi lại mã").
- Quản lý
AbortControllertrên tất cả các đường dẫn thoát: TruyềnAbortSignal(signal: ac.signal) đếnnavigator.credentials.get(). Gọi choac.abort()bất cứ khi nào:- Người dùng gửi biểu mẫu theo cách thủ công (hoặc gửi mã được điền qua
autocomplete="one-time-code"). - Một mã SMS mới được yêu cầu ("Gửi lại mã"), huỷ bỏ trình điều khiển trước đó trước khi tạo một trình điều khiển mới.
- Thành phần, hộp thoại hoặc tuyến xác minh sẽ bị huỷ.
- Người dùng gửi biểu mẫu theo cách thủ công (hoặc gửi mã được điền qua
- Kích hoạt trình xử lý gửi của ứng dụng (
form.requestSubmit()): Khinavigator.credentials.get()phân giải bằngotp, hãy đặtotpInput.value = otp.codevà gọiform.requestSubmit()(hoặc gọi trực tiếp hàm xác minh của bạn). KHÔNG gọiform.submit(), vìform.submit()bỏ qua trình nghe sự kiệnsubmit(trình xử lýevent.preventDefault()/fetch()) và bỏ qua quy trình xác thực quy tắc ràng buộc gốc.
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();
Xác minh iframe khác nguồn gốc
Theo mặc định, việc nhận OTP qua SMS bên trong một <iframe> khác nguồn gốc sẽ bị hạn chế và chủ yếu được dùng cho các quy trình xác nhận thanh toán được nhúng (chẳng hạn như quy trình xác minh 3D Secure, trong đó shop.example nhúng một thử thách của tổ chức phát hành từ bank.example). Để hỗ trợ WebOTP bên trong một <iframe> khác nguồn gốc, bạn cần có 3 cấu hình phối hợp:
Liên kết cả hai nguồn gốc trên dòng cuối cùng của tin nhắn SMS: Liệt kê miền nhúng cấp cao nhất trước (
@shop.example), theo sau là#<otp>, theo sau là dấu cách và miền iframe được nhúng (@bank.example):Your verification code is 123456. @shop.example #123456 @bank.exampleGửi tiêu đề phản hồi HTTP
Permissions-Policy: Uỷ quyềnotp-credentialscho nguồn gốc iframe khác nguồn gốc trong tiêu đề HTTP của tài liệu gốc:Permissions-Policy: otp-credentials=(self "https://bank.example")Thêm
allow="otp-credentials"vào phần tử<iframe>: Cấp rõ ràng chính sách quyền trên thẻ<iframe>trong trang nhúng và thêm thuộc tínhtitlemô tả cho trình đọc màn hình:
<iframe
src="https://bank.example/verify"
allow="otp-credentials"
title="Payment verification">
</iframe>
Bên trong tài liệu <iframe> do https://bank.example phân phát, hãy chạy cùng một mã đánh dấu <form> và vòng đời xác minh navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal }) như trình bày ở trên.
Chiến lược dự phòng
Dự phòng WebOTP API
Nếu trình duyệt mục tiêu của bạn không hỗ trợ WebOTP API (OTPCredential), đừng tải polyfill JavaScript của bên thứ ba, vì việc đọc tin nhắn SMS đến yêu cầu tích hợp ở cấp hệ điều hành. Thay vào đó, hãy dựa vào tính năng cải tiến tăng dần ba cấp được tích hợp trong mã đánh dấu <input>:
- Trải nghiệm cải tiến tăng dần: Bảo vệ lệnh gọi
navigator.credentials.get({ otp: { transport: ['sms'] } })sauif ('OTPCredential' in window). Khi không cóOTPCredential:- Các trình duyệt hỗ trợ
autocomplete="one-time-code"(chẳng hạn như Safari trên iOS 14 trở lên, iPadOS và macOS có tính năng chuyển tiếp SMS iCloud) sẽ phân tích cú pháp SMS@domain #codevà hiển thị mã trong thanh tự động điền của bàn phím hệ thống khi<input>được lấy tiêu điểm. - Tất cả các trình duyệt khác đều quay lại chế độ nhập thủ công trong một
<input type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" required>, hiển thị bàn phím số trên thiết bị di động và gửi qua<button type="submit">.
- Các trình duyệt hỗ trợ
Dự phòng Chính sách quyền
Nếu trình duyệt không hỗ trợ việc uỷ quyền otp-credentials thông qua Permissions-Policy và <iframe allow="otp-credentials">, thì quy trình xác minh được nhúng <form> bên trong <iframe> khác nguồn gốc sẽ giảm xuống thành các đề xuất bàn phím autocomplete="one-time-code" và nhập số theo cách thủ công mà không gửi các ngoại lệ chưa được xử lý.
Hỗ trợ Inputmode và AbortController
Cả thuộc tính HTML inputmode và AbortController (AbortSignal) đều được hỗ trợ trên các trình duyệt hiện đại và không cần mã polyfill hoặc mã dự phòng.