การส่งรหัสผ่านแบบใช้งานครั้งเดียว (OTP) ผ่าน SMS เป็นขั้นตอนที่พบบ่อยสำหรับการยืนยันหมายเลขโทรศัพท์ การกู้คืนบัญชี การตรวจสอบสิทธิ์แบบ Step-up และการยืนยันการชำระเงิน การสลับไปมาระหว่างเบราว์เซอร์กับแอปรับส่งข้อความ SMS เพื่อจดจำหรือคัดลอกและวางรหัสหลายหลักทำให้ผู้ใช้เกิดความยุ่งยาก เพิ่มอัตราการออกจากเว็บไซต์ และทำให้ผู้ใช้เสี่ยงต่อฟิชชิงในโดเมนที่มีการปลอมแปลง
การรวมมาร์กอัป <form> เชิงความหมาย, <input autocomplete="one-time-code" inputmode="numeric">, การจัดรูปแบบ SMS ที่เชื่อมโยงกับต้นทาง (@domain #code) และ WebOTP API (OTPCredential) จะสร้างขั้นตอนการยืนยันที่ได้รับการเพิ่มประสิทธิภาพแบบต่อเนื่องแบบเป็นชั้นๆ ดังนี้
- การดึงข้อมูลและการส่งโดยอัตโนมัติ: เบราว์เซอร์ที่รองรับ WebOTP API จะยืนยันต้นทางกับบรรทัด
@domain #codeใน SMS, ป้อน<input>และทริกเกอร์การยืนยันด้วยการแตะเพียงครั้งเดียว - การป้อนข้อความอัตโนมัติบนแป้นพิมพ์ที่เชื่อมโยงกับต้นทาง: เบราว์เซอร์และระบบปฏิบัติการที่รองรับ
autocomplete="one-time-code"(เช่น Safari บน iOS, iPadOS และ macOS) จะแยกวิเคราะห์ข้อความ@domain #codeและแนะนำรหัสในแถบป้อนข้อความอัตโนมัติบนแป้นพิมพ์เฉพาะเมื่อโดเมนตรงกันเท่านั้น - การป้อนข้อมูลด้วยตนเองที่ราบรื่น: ผู้ใช้ที่ป้อนรหัสจากอุปกรณ์อื่นจะได้รับปุ่มกดเสมือนที่เป็นตัวเลขผ่าน
inputmode="numeric"โดยไม่มีอันตรายด้านความสามารถในการใช้งานของ<input type="number">
ข้อควรพิจารณาด้านความปลอดภัย
OTP ทาง SMS จะยืนยันการเป็นเจ้าของหมายเลขโทรศัพท์และให้การยืนยันแบบ Step-up ที่สะดวก แต่การส่ง SMS และหมายเลขโทรศัพท์อาจเสี่ยงต่อการสลับซิม การรีไซเคิลหมายเลข และการดักฟัง จับคู่การป้อนข้อความอัตโนมัติฝั่งไคลเอ็นต์และ WebOTP API กับรูปแบบ SMS @domain #code ที่เชื่อมโยงกับต้นทางเสมอ เพื่อให้เบราว์เซอร์และระบบปฏิบัติการยืนยันว่าโดเมนเป้าหมายตรงกันก่อนที่จะแนะนำหรือส่งรหัส สำหรับการตรวจสอบสิทธิ์หลักที่ทนทานต่อฟิชชิง ให้เลือกใช้พาสคีย์แทนรหัสที่ส่งทาง SMS (ดูการทำความเข้าใจพาสคีย์)
มาร์กอัปแบบฟอร์มเชิงความหมาย
รวมการควบคุมการยืนยันไว้ในองค์ประกอบ <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": สั่งให้เบราว์เซอร์และระบบปฏิบัติการแนะนำรหัสยืนยันทาง SMS ที่เข้ามาในแถบป้อนข้อความอัตโนมัติเมื่อโฟกัสที่<input> - วางคำแนะนำไว้เหนือ
<input>: เชื่อมโยง<label>ที่มองเห็นได้โดยใช้forและidและวางคำแนะนำรูปแบบ (ลิงก์ผ่านaria-describedby) ไว้เหนือ<input>เพื่อไม่ให้ป๊อปโอเวอร์การป้อนข้อความอัตโนมัติและแป้นพิมพ์เสมือนบนอุปกรณ์เคลื่อนที่บดบังคำแนะนำ - อย่าแยกตัวเลขออกเป็นองค์ประกอบ
<input>หลายรายการ: รูปแบบ OTP แบบหลายช่อง (อินพุตmaxlength="1"แยกกัน 6 ช่อง) จะทำให้การป้อนข้อความอัตโนมัติautocomplete="one-time-code"แบบเนทีฟในเบราว์เซอร์หลายรายการใช้งานไม่ได้ ขัดขวางการคัดลอกและวาง และทำให้การไปยังส่วนต่างๆ ของโปรแกรมอ่านหน้าจอซับซ้อนขึ้น หากจำเป็นต้องใช้การออกแบบภาพแบบแบ่งส่วน ให้จัดรูปแบบ<input>รายการเดียวด้วยletter-spacingและแบบอักษรที่มีความกว้างอักษรขนาดเดียว (Monospace)
<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;
}
รูปแบบข้อความ SMS ที่เชื่อมโยงกับต้นทาง
เพื่อให้ทั้ง WebOTP API และการป้อนข้อความอัตโนมัติ autocomplete="one-time-code" ที่เชื่อมโยงกับโดเมนของ Safari ยืนยันได้ว่ารหัสนั้นเป็นของเว็บไซต์คุณ บริการ SMS แบ็กเอนด์ต้องจัดรูปแบบบรรทัดสุดท้ายของ SMS โดยให้โดเมนต้นทางขึ้นต้นด้วย @ และ OTP ขึ้นต้นด้วย # ดังนี้
Your verification code is 123456.
@example.com #123456
บังคับใช้กฎการจัดรูปแบบต่อไปนี้กับข้อความ SMS ขาออก
- ใส่ข้อความที่มนุษย์อ่านได้ก่อน: เริ่มต้น SMS ด้วยประโยคที่มนุษย์อ่านได้ชัดเจนซึ่งมีรหัสอยู่เสมอ เพื่อให้ผู้ใช้ที่พิมพ์รหัสด้วยตนเองในอุปกรณ์อื่นอ่านได้ง่าย
- วาง
@<domain> #<otp>ไว้ที่บรรทัดสุดท้าย: อักขระ@ต้องเป็นอักขระตัวแรกของบรรทัดสุดท้าย โดยไม่มีบรรทัดต่อท้ายหรือข้อความลายเซ็นต่อท้าย - ใช้เฉพาะโดเมนโฮสต์: อย่าใส่ URL Scheme (
https://หรือhttp://), พอร์ต (:8080) หรือเส้นทาง (/verify) ในโทเค็น@domain - แยกโทเค็นด้วยการเว้นวรรค 1 ครั้ง: เว้นวรรค 1 ครั้งระหว่าง
@<domain>และ#<otp>และอย่าเว้นวรรคภายในโดเมนหรือระหว่าง#กับโค้ด
การผสานรวม WebOTP API เข้ากับขั้นตอนการยืนยัน
WebOTP API (OTPCredential) ต้องใช้บริบทที่ปลอดภัย (https:// ในเวอร์ชันที่ใช้งานจริง หรือ http://localhost ระหว่างการพัฒนาในเครื่อง) แทนที่จะวางสคริปต์ DOMContentLoaded ที่ไม่ได้เชื่อมต่อซึ่งค้นหา DOM และเรียกใช้ form.submit() ให้ผสานรวม navigator.credentials.get() เข้ากับวงจรและตัวแฮนเดิลการส่งของ UI การยืนยันโดยตรง ดังนี้
- ผูกวงจรคำขอกับขั้นตอน OTP ที่ใช้งานอยู่: เริ่มฟัง OTP เมื่ออินพุตการยืนยันแสดงผลและใช้งานอยู่ (เช่น เมื่อขั้นตอน OTP เริ่มทำงานหลังจากที่ผู้ใช้ขอรหัส SMS และอีกครั้งเมื่อผู้ใช้คลิกปุ่ม "ส่งรหัสอีกครั้ง")
- จัดการ
AbortControllerในเส้นทางออกทั้งหมด: ส่งAbortSignal(signal: ac.signal) ไปยังnavigator.credentials.get()โทรหาac.abort()ได้ทุกเมื่อเพื่อดำเนินการต่อไปนี้- ผู้ใช้ส่งแบบฟอร์มด้วยตนเอง (หรือส่งรหัสที่กรอกผ่าน
autocomplete="one-time-code") - มีการขอรหัส SMS ใหม่ ("ส่งรหัสอีกครั้ง") ซึ่งจะยกเลิกตัวควบคุมก่อนหน้าก่อนที่จะสร้างตัวควบคุมใหม่
- คอมโพเนนต์ กล่องโต้ตอบ หรือเส้นทางการยืนยันจะยกเลิกการต่อเชื่อม
- ผู้ใช้ส่งแบบฟอร์มด้วยตนเอง (หรือส่งรหัสที่กรอกผ่าน
- ทริกเกอร์เครื่องมือจัดการการส่งของแอปพลิเคชัน (
form.requestSubmit()): เมื่อnavigator.credentials.get()แก้ไขด้วยotpให้ตั้งค่าotpInput.value = otp.codeและเรียกใช้form.requestSubmit()(หรือเรียกใช้ฟังก์ชันการยืนยันโดยตรง) อย่าเรียกใช้form.submit()เนื่องจากform.submit()จะข้าม Listener เหตุการณ์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 แบบข้ามต้นทาง
โดยค่าเริ่มต้น ระบบจะจำกัดการรับ OTP ทาง SMS ภายใน <iframe> แบบข้ามต้นทาง และใช้สำหรับการยืนยันการชำระเงินแบบฝังเป็นหลัก (เช่น การยืนยัน 3D Secure ที่ shop.example ฝังการตรวจสอบของผู้ออกบัตรจาก bank.example) การรองรับ WebOTP ภายใน <iframe> แบบข้ามต้นทางต้องมีการกำหนดค่าที่สอดคล้องกัน 3 รายการดังนี้
เชื่อมโยงทั้ง 2 ต้นทางในบรรทัดสุดท้ายของ SMS: ระบุโดเมนที่ฝังระดับบนสุดก่อน (
@shop.example) ตามด้วย#<otp>ตามด้วยเว้นวรรคและโดเมน iframe ที่ฝัง (@bank.example) ดังนี้Your verification code is 123456. @shop.example #123456 @bank.exampleส่งส่วนหัวการตอบกลับ HTTP
Permissions-Policy: มอบสิทธิ์otp-credentialsให้กับต้นทาง iframe แบบข้ามต้นทางในส่วนหัว HTTP ของเอกสารหลักPermissions-Policy: otp-credentials=(self "https://bank.example")เพิ่ม
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) อย่าโหลด Polyfill ของ JavaScript ของบุคคลที่สาม เนื่องจากการอ่านข้อความ SMS ขาเข้าต้องมีการผสานรวมระดับระบบปฏิบัติการ แต่ให้ใช้การเพิ่มประสิทธิภาพแบบต่อเนื่อง 3 ระดับที่สร้างขึ้นในมาร์กอัป <input> แทน ดังนี้
- ประสบการณ์การเพิ่มประสิทธิภาพแบบต่อเนื่อง: ปกป้องการเรียกใช้
navigator.credentials.get({ otp: { transport: ['sms'] } })ไว้หลังif ('OTPCredential' in window)เมื่อOTPCredentialไม่พร้อมใช้งาน ให้ทำดังนี้- เบราว์เซอร์ที่รองรับ
autocomplete="one-time-code"(เช่น Safari ใน iOS 14 ขึ้นไป, iPadOS และ macOS ที่มีรีเลย์ SMS ของ iCloud) จะแยกวิเคราะห์ SMS@domain #codeและแสดงรหัสในแถบป้อนข้อความอัตโนมัติของแป้นพิมพ์ระบบเมื่อโฟกัสที่<input> - เบราว์เซอร์อื่นๆ ทั้งหมดจะเปลี่ยนกลับไปใช้การป้อนข้อมูลด้วยตนเองใน
<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
ทั้งแอตทริบิวต์ inputmode ของ HTML และ AbortController (AbortSignal) ได้รับการรองรับในเบราว์เซอร์ที่ทันสมัย และไม่จำเป็นต้องใช้โค้ด Polyfill หรือโค้ดสำรอง