Skip to main content

[DDD] Thiết kế Chi tiết Trang Xác Thực OTP (Quên Mật khẩu)

Tóm tắt tài liệu

Tài liệu này hướng dẫn thiết kế trang Xác Thực OTP và Đặt Lại Mật Khẩu. Đây là bước cuối trong quá trình khôi phục tài khoản - người dùng nhập mã OTP 6 số từ tin nhắn và tạo mật khẩu mới. Trang được thiết kế đơn giản nhưng bảo mật cao, phù hợp với người dùng Việt Nam.

1. Tổng quan (Overview)

1.1. Trang này dùng để làm gì?

Khi người dùng quên mật khẩu và đã nhập số điện thoại, họ sẽ nhận được tin nhắn chứa mã OTP 6 số. Trang này giúp họ:

  1. Nhập mã OTP để xác thực danh tính
  2. Tạo mật khẩu mới cho tài khoản
  3. Hoàn tất quá trình khôi phục tài khoản

Ví dụ thực tế: Giống như khi bạn quên mật khẩu ngân hàng online, sau khi nhập số điện thoại, ngân hàng gửi mã OTP. Bạn nhập mã này và đặt mật khẩu mới.

1.2. Những thách thức cần giải quyết

🔐 Về bảo mật:

  • Mã OTP chỉ có hiệu lực 5 phút
  • Chỉ được thử sai tối đa 5 lần
  • Mật khẩu mới phải đủ mạnh theo quy định

👤 Về trải nghiệm người dùng:

  • Tự động chuyển ô khi nhập OTP
  • Hỗ trợ dán mã từ tin nhắn
  • Hiển thị đồng hồ đếm ngược rõ ràng
  • Phân biệt các loại lỗi khác nhau

📱 Về thiết bị:

  • Hoạt động mượt mà trên điện thoại và máy tính
  • Hỗ trợ bàn phím ảo trên di động

1.3. Mục tiêu & Phi mục tiêu (Goals and Non-Goals)

Mục tiêu (Goals)

Phần nhập OTP:

  • Tự động chuyển ô khi nhập số
  • Đồng hồ đếm ngược 5 phút rõ ràng
  • Nút "Gửi lại OTP" (chỉ được bấm sau 1 phút)
  • Xác thực mã OTP an toàn trên server

Phần tạo mật khẩu:

  • Nút hiện/ẩn mật khẩu với biểu tượng con mắt
  • Kiểm tra độ mạnh mật khẩu theo thời gian thực
  • Xác nhận mật khẩu phải trùng khớp
  • Danh sách yêu cầu mật khẩu hiển thị rõ ràng

Điều hướng và UX:

  • Nút quay lại trang trước
  • Thông báo thành công/lỗi bằng toast
  • Hiện trạng thái "Đang tải..." khi gọi API
  • Tự động gửi form khi đã nhập đủ 6 số OTP

Phi mục tiêu (Non-Goals)

  • Không hỗ trợ xác thực nhiều bước (Multi-Factor Authentication)
  • Không kiểm tra mật khẩu cũ đã dùng trước đó
  • Không tích hợp đăng nhập bằng Google/Facebook
  • Không gửi email thông báo
  • Không có tùy chọn khôi phục nâng cao khác

2. Kiến trúc Tổng thể (Overall Architecture)

Trang này kết hợp 2 chức năng chính: Xác thực OTPĐặt mật khẩu mới trong một biểu mẫu duy nhất.

Cấu trúc trang web (Client)
├── ConfirmForgotPasswordPage (Trang chính)
│ ├── Phần Header
│ │ ├── Tiêu đề: "Đặt mật khẩu mới"
│ │ └── Phụ đề: "Nhập mã OTP và mật khẩu mới của bạn"
│ ├── Phần nhập OTP
│ │ ├── 6 ô vuông để nhập số (□□□□□□)
│ │ └── Hiển thị lỗi nếu có
│ ├── Phần tạo mật khẩu
│ │ ├── Ô nhập mật khẩu mới + nút hiện/ẩn
│ │ ├── Ô xác nhận mật khẩu + nút hiện/ẩn
│ │ └── Danh sách yêu cầu mật khẩu
│ ├── Phần hành động
│ │ ├── Nút "Tiếp tục" (gửi form)
│ │ └── Nút "Gửi lại OTP" + đồng hồ đếm ngược
│ └── Phần điều hướng
│ └── Nút "Quay lại"

Quản lý trạng thái (State Management)
├── Trạng thái OTP (6 số)
├── Trạng thái mật khẩu
├── Trạng thái xác nhận mật khẩu
├── Trạng thái hiện/ẩn mật khẩu
├── Trạng thái đồng hồ đếm ngược
├── Trạng thái đang tải

Kết nối API (API Layer)
├── POST /auth/api/v1/verify-otp (Xác thực OTP)
├── POST /auth/api/v1/reset-password (Đặt lại mật khẩu)

Kiểm tra dữ liệu (Validation)
├── OTP: Chỉ được 6 số
├── Mật khẩu: Phải đủ mạnh theo quy định

Sequence Diagram - Luồng hoạt động Trang Xác Thức OTP

Giải thích các bước quan trọng:

  1. Khởi tạo: Tự động focus và bắt đầu đếm ngược
  2. Nhập OTP: Hỗ trợ cả nhập từng số và paste chuỗi
  3. Validation realtime: Kiểm tra ngay khi người dùng nhập
  4. Auto-submit: Có thể tự động gửi khi đủ 6 số OTP
  5. Error handling: Phân biệt rõ các loại lỗi khác nhau
  6. Resend logic: Có countdown ngăn spam
  7. Navigation: An toàn với xác nhận trước khi rời trang

3. Thiết kế Chi tiết (Detailed Design)

3.1. Hành trình của người dùng từng bước

🎬 Bước 1: Mở trang

  • Hệ thống lấy số điện thoại từ URL (ví dụ: /confirm-otp?phone=84909172413)
  • Tự động focus vào ô OTP đầu tiên (người dùng có thể nhập ngay)
  • Bắt đầu đếm ngược từ 5:00 (5 phút)
  • Hiển thị số điện thoại đã được che: "+84909***413"

📱 Bước 2: Nhập mã OTP

Cách 1 - Nhập từng số:

  1. Người dùng nhập số "1" vào ô đầu tiên
  2. Con trỏ tự động chuyển sang ô thứ 2
  3. Tiếp tục cho đến ô thứ 6
  4. Khi đủ 6 số, nút "Tiếp tục" sáng lên

Cách 2 - Dán từ tin nhắn:

  1. Người dùng copy mã "123456" từ tin nhắn SMS
  2. Click vào bất kỳ ô OTP nào và paste (Ctrl+V)
  3. Hệ thống tự động tách thành: 1|2|3|4|5|6
  4. Con trỏ di chuyển đến ô cuối cùng

🔒 Bước 3: Tạo mật khẩu mới

  1. Nhập mật khẩu: Người dùng gõ mật khẩu mới

    • Hệ thống kiểm tra ngay lập tức:
    • ✅ Có ít nhất 8 ký tự
    • ✅ Có chữ hoa (A-Z)
    • ✅ Có chữ thường (a-z)
    • ✅ Có số (0-9)
    • ✅ Có ký tự đặc biệt (!@#$...)
  2. Xác nhận mật khẩu: Nhập lại mật khẩu

    • Nếu trùng khớp: hiện dấu tick xanh ✅
    • Nếu không khớp: hiện cảnh báo đỏ ❌
  3. Hiện/ẩn mật khẩu: Click biểu tượng con mắt để xem mật khẩu

🚀 Bước 4: Gửi form

  1. Người dùng click "Tiếp tục"
  2. Hệ thống kiểm tra lần cuối:
    • OTP có đủ 6 số không?
    • Mật khẩu có đạt yêu cầu không?
    • Xác nhận mật khẩu có khớp không?
  3. Gửi thông tin lên server để xác thực
  4. Nếu thành công: chuyển đến trang đăng nhập với thông báo "Đặt mật khẩu thành công!"

🔄 Bước 5: Gửi lại OTP (nếu cần)

  1. Nếu người dùng click "Gửi lại OTP":
    • Kiểm tra đã hết thời gian chờ 1 phút chưa
    • Nếu chưa: hiện thông báo "Vui lòng chờ X giây nữa"
    • Nếu đã hết: gửi SMS mới và reset đồng hồ về 5:00

3.2. Thiết kế API (API Design)

API chính: Xác thực OTP và Đặt mật khẩu

Mục đích: Xác thực mã OTP và cập nhật mật khẩu mới cùng lúc

Endpoint: POST /auth/api/v1/confirm-forgot-password

Dữ liệu gửi đi:

{
"phone_number": "+84909172413",
"confirmation_code": "123456",
"new_password": "MyNewPassword123!"
}

Khi thành công (200):

{
"success": true,
"message": "Đặt mật khẩu thành công",
"data": {
"user_id": "uuid-string",
"phone_number": "+84909172413"
}
}

Khi có lỗi (422):

   {
"success": false,
"message": "Mã OTP không đúng",
"errors": [
{
"field": "confirmation_code",
"message": "Mã OTP đã hết hạn hoặc không đúng",
"code": "INVALID_OTP"
}
]
}

Các loại lỗi thường gặp:

  • INVALID_OTP: Mã OTP sai
  • EXPIRED_OTP: Mã OTP hết hạn
  • USED_OTP: Mã OTP đã được sử dụng
  • TOO_MANY_ATTEMPTS: Thử quá nhiều lần
  • WEAK_PASSWORD: Mật khẩu không đủ mạnh
  • PHONE_NOT_FOUND: Số điện thoại không tồn tại

API phụ: Gửi lại OTP

Endpoint: POST /auth/api/v1/resend-otp

Dữ liệu gửi đi:

{
"phone_number": "+84909172413"
}

Khi thành công:

{
"success": true,
"message": "Đã gửi lại mã OTP",
"data": {
"expires_in": 300
}
}

4. Các yếu tố phi chức năng (Non-Functional Requirements)

4.1. Hiệu suất (Performance)

⚡ Tốc độ tải trang:

  • Trang phải tải xong trong dưới 2 giây
  • Tự động focus vào ô OTP trong dưới 0.5 giây

⚡ Phản hồi thời gian thực:

  • Kiểm tra mật khẩu mạnh/yếu trong dưới 100ms
  • Chuyển ô OTP trong dưới 50ms
  • Đếm ngược mượt mà, chính xác từng giây

⚡ Gọi API:

  • Xác thực OTP hoàn tất trong dưới 2 giây
  • Gửi lại OTP trong dưới 3 giây
  • Timeout sau 10 giây nếu không có phản hồi

4.2. Bảo mật (Security)

🔐 Bảo vệ OTP:

  • Giới hạn thử: Chỉ được thử sai tối đa 5 lần
  • Hết hạn: OTP chỉ có hiệu lực 5 phút
  • Một lần dùng: Mỗi OTP chỉ dùng được 1 lần
  • Xác thực server: Tất cả kiểm tra OTP đều ở server

🔐 Bảo vệ mật khẩu:

  • Mã hóa: Dùng bcrypt với độ phức tạp 12
  • Quy tắc mạnh: Tối thiểu 8 ký tự, có chữ hoa, thường, số, ký tự đặc biệt
  • Không lưu: Mật khẩu không được lưu ở browser

🔐 Bảo vệ chung:

  • HTTPS: Tất cả giao tiếp đều mã hóa
  • Rate limiting: Gửi lại OTP tối đa mỗi 60 giây
  • Làm sạch input: Loại bỏ ký tự độc hại

4.3. Khả năng mở rộng (Scalability)

📊 Cơ sở dữ liệu:

  • Tạo chỉ mục (index) cho tra cứu OTP nhanh
  • Tự động xóa OTP hết hạn để tiết kiệm dung lượng

📊 Bộ nhớ:

  • Dọn dẹp timer khi người dùng rời trang
  • Không giữ dữ liệu nhạy cảm trong localStorage

📊 SMS:

  • Giới hạn số lượng SMS gửi để tránh spam
  • Hỗ trợ nhiều nhà cung cấp SMS dự phòng

4.4. Trải nghiệm người dùng (UX)

👆 Dễ sử dụng:

  • Tự động focus: Không cần click thủ công
  • Paste thông minh: Hỗ trợ dán chuỗi 6 số
  • Visual feedback: Màu sắc rõ ràng cho trạng thái

💬 Thông báo rõ ràng:

  • Lỗi cụ thể: "Mã OTP sai" thay vì "Lỗi"
  • Hướng dẫn: "Kiểm tra tin nhắn trong hộp thư đến"
  • Toast thông minh: Tự động biến mất sau 3 giây

♿ Accessibility:

  • Screen reader: Hỗ trợ người khiếm thị
  • Keyboard navigation: Dùng Tab/Shift+Tab để di chuyển
  • Contrast cao: Màu sắc phân biệt rõ ràng

4.5. Tương thích (Compatibility)

🌐 Trình duyệt hỗ trợ:

  • Chrome 90+ (Khuyến nghị)
  • Firefox 88+
  • Safari 14+ (iPhone/iPad)
  • Edge 90+ (Windows)
  • Internet Explorer (Không hỗ trợ)

📱 Thiết bị di động:

  • Kích thước: Hoạt động tốt từ màn hình 320px trở lên
  • Touch: Ô nhập đủ lớn cho ngón tay (tối thiểu 44px)
  • Keyboard ảo: Tự động hiện bàn phím số khi focus vào OTP

⌨️ Phương thức nhập:

  • Bàn phím vật lý: Cho máy tính
  • Bàn phím ảo: Cho điện thoại/tablet
  • Paste: Hỗ trợ Ctrl+V hoặc long press

5. Rủi ro và Phương án giảm thiểu (Risks and Mitigation)

Rủi roMức độ ảnh hưởngKhả năng xảy raPhương án giảm thiểu
Kẻ xấu tấn công brute force OTPCaoTrung bình- Chỉ cho thử 5 lần
- Khóa tạm thời sau 5 lần sai
- Cảnh báo qua email khi có hoạt động đáng ngờ
Không nhận được tin nhắn OTPCaoThấp- Dùng nhiều nhà cung cấp SMS
- Hướng dẫn kiểm tra spam/chặn tin nhắn
- Cung cấp số hotline hỗ trợ
Đồng hồ đếm ngược không chính xácTrung bìnhThấp- Đồng bộ thời gian với server
- Gia hạn thêm 30 giây cho phép sai số
- Thông báo rõ khi OTP hết hạn
Mật khẩu yếu vượt qua kiểm traCaoThấp- Kiểm tra ở cả client và server
- Dùng regex pattern chặt chẽ
- Kiểm tra định kỳ bảo mật
Tự động submit khi chưa sẵn sàngThấpTrung bình- Thêm delay 200ms trước khi submit
- Cho phép người dùng cancel
- Xác nhận trước khi gửi

6. Kế hoạch Testing (Testing Strategy)

6.1. Kiểm thử Chức năng (Functional Testing)

🧪 Test nhập OTP:

// Test tự động chuyển ô
it('should auto-focus next input when entering digit', () => {
// Nhập "1" vào ô đầu tiên
// Expect: Focus chuyển sang ô thứ 2
});

// Test paste OTP
it('should paste 6-digit string into all boxes', () => {
// Paste "123456" vào ô bất kỳ
// Expect: Các ô hiển thị 1|2|3|4|5|6
});

🧪 Test validation mật khẩu:

// Test mật khẩu yếu
it('should show weak password error', () => {
// Nhập "123" vào ô mật khẩu
// Expect: Hiện lỗi "Mật khẩu quá yếu"
});

// Test mật khẩu không khớp
it('should show mismatch error', () => {
// Mật khẩu: "Strong123!", Xác nhận: "Strong456!"
// Expect: Hiện lỗi "Mật khẩu không khớp"
});

6.2. Kiểm thử Tích hợp (Integration Testing)

🔗 Test kết nối API:

  • Mock API trả về các trường hợp: thành công, OTP sai, OTP hết hạn
  • Test timeout khi API không phản hồi
  • Test retry logic khi mạng chập chờn

🔗 Test luồng điều hướng:

  • Test chuyển trang sau khi reset mật khẩu thành công
  • Test nút "Quay lại" hoạt động đúng
  • Test lưu/xóa dữ liệu tạm thời

6.3. Kiểm thử Giao diện (UI Testing)

📱 Test responsive:

// Test trên mobile
it('should work on mobile viewport', () => {
cy.viewport('iphone-6');
cy.visit('/confirm-otp');
cy.get('[data-testid="otp-input"]').should('be.visible');
});

// Test keyboard ảo
it('should show numeric keypad on mobile', () => {
// Expect: Bàn phím số hiện lên khi focus OTP
});

🖱️ Test accessibility:

it('should be keyboard navigable', () => {
// Test Tab key để di chuyển giữa các trường
// Test phím mũi tên trong OTP inputs
// Test Enter để submit form
});

6.4. Kiểm thử Bảo mật (Security Testing)

🛡️ Test OTP brute force:

it('should block after 5 wrong attempts', () => {
// Gửi 5 OTP sai liên tiếp
// Expect: Tài khoản bị khóa tạm thời
});

🛡️ Test SQL injection:

it('should handle malicious input', () => {
// Nhập các ký tự đặc biệt như '; DROP TABLE--
// Expect: Hệ thống từ chối hoặc escape an toàn
});

7. Triển khai và Giám sát

7.1. Chuẩn bị triển khai

⚙️ Biến môi trường:

Thời gian hết hạn OTP (giây)

OTP_EXPIRES_IN=300
Số lần thử tối đa

MAX_OTP_ATTEMPTS=5
Khoảng cách tối thiểu giữa 2 lần gửi OTP (giây)

RESEND_OTP_COOLDOWN=60
URL chuyển hướng sau khi thành công

SUCCESS_REDIRECT_URL=/login

📊 Cơ sở dữ liệu:

-- Thêm cột theo dõi số lần thử OTP
ALTER TABLE password_reset_tokens
ADD COLUMN attempt_count INT DEFAULT 0;

-- Thêm index để truy vấn nhanh
CREATE INDEX idx_phone_otp ON password_reset_tokens(phone_number, confirmation_code);

🚀 Feature flags:

// Bật/tắt tự động submit khi nhập đủ OTP
AUTO_SUBMIT_OTP=true

// Bật/tắt tính năng paste OTP
ENABLE_OTP_PASTE=true

7.2. Giám sát và Phân tích

📈 Metrics quan trọng:

Tỷ lệ thành công:

  • % người dùng hoàn thành được reset mật khẩu
  • % OTP được xác thực thành công lần đầu
  • Thời gian trung bình để hoàn thành toàn bộ quá trình

Lỗi phổ biến:

  • Số lần OTP bị nhập sai
  • Số lần timeout API
  • Số lượng mật khẩu bị từ chối vì quá yếu

Bảo mật:

  • Số lần thử brute force OTP
  • Số tài khoản bị khóa do thử quá nhiều lần
  • Hoạt động đáng ngờ (VD: cùng IP thử nhiều số điện thoại)

📊 Dashboard theo dõi:

// Ví dụ metric cần track
{
"otp_verification_success_rate": 85.2, // %
"average_completion_time": 45, // giây
"password_strength_rejection_rate": 12.1, // %
"resend_otp_usage": 23.4, // % người dùng cần gửi lại
"mobile_vs_desktop_usage": {
"mobile": 67.8, // %
"desktop": 32.2 // %
}
}

🚨 Cảnh báo tự động:

  • Tỷ lệ thành công OTP < 80%
  • Thời gian phản hồi API > 5 giây
  • Có > 10 lần thử brute force trong 1 giờ
  • Dịch vụ SMS gặp sự cố
Tài liệu sống

Đây là tài liệu được cập nhật thường xuyên. Mọi thay đổi về tính năng, API, hoặc quy trình sẽ được phản ánh ngay lập tức tại đây.

Lần cập nhật gần nhất: 24/07/2025 Lần review tiếp theo: 01/08/2025


Tài liệu tham khảo: