Một lỗi đăng nhập bắt nguồn từ việc lưu trữ số điện thoại không đồng nhất đã làm lộ ra một lỗ hổng. Tài khoản của một người dùng đã bị chặn vì cơ sở dữ liệu lưu cùng một số di động của Đức dưới hai định dạng khác nhau—0171 5550134 ở một dòng và +49 171 5550134 ở một dòng khác—khiến hệ thống coi chúng là các mục riêng biệt. Kết quả là: mã OTP không bao giờ được gửi đến, và nút “gửi mã” trở thành một trò may rủi.
Tại sao số điện thoại lại khó xử lý hơn ngày tháng
Các nhà phát triển thường tin tưởng vào regex để kiểm soát dữ liệu đầu vào của số điện thoại. Niềm tin đó sẽ tan biến ngay khi một số điện thoại vượt qua biên giới hoặc khi kế hoạch viễn thông quốc gia thay đổi. Ngày tháng tuân theo một lịch trình có thể dự đoán được; còn số điện thoại thì biến đổi theo nhà mạng, các quy định và quy ước văn hóa.
Chi phí ẩn của các chuỗi thô (raw strings)
Lưu trữ số điện thoại dưới dạng một chuỗi văn bản thuần túy trông có vẻ là duy nhất—cho đến khi hai cách biểu diễn khác nhau cùng trỏ về một số điện thoại. Các hệ thống OTP khi truy vấn cơ sở dữ liệu để tìm "số điện thoại đó" cuối cùng lại gửi mã đến một bản ghi trùng lặp mà không bao giờ đến được tay người dùng.
Vấn đề trở nên tồi tệ hơn khi các số này nằm trong các trường dữ liệu kiểu số. Một cột BIGINT sẽ loại bỏ dấu + ở đầu và bất kỳ số 0 nào ở đầu, biến +49 171 5550134 thành 491715550134. Nếu không có dấu cộng, việc khôi phục lại định dạng ban đầu chỉ còn là một sự phỏng đoán.
Quy ước E.164
Kế hoạch đánh số điện thoại quốc tế, E.164, định nghĩa một cách biểu diễn duy nhất và có tính di động:
- Bắt đầu bằng dấu
+ - Theo sau là mã quốc gia từ 1 đến 3 chữ số
- Sau đó là số thuê bao
- Tổng cộng không quá 15 chữ số
- Không có khoảng trắng, dấu chấm hoặc dấu gạch ngang
E.164 không đảm bảo số điện thoại đó đang hoạt động; nó chỉ đảm bảo chuỗi tuân theo một cấu trúc hợp lệ. Hãy coi nó như một quy ước về định dạng, chứ không phải là một công cụ xác thực khả năng kết nối.
Những sai lầm phổ biến mà regex không thể khắc phục
- Lưu trữ kiểu số (Numeric storage) –
BIGINTsẽ loại bỏ dấu+và các số 0 ở đầu. Thay vào đó, hãy sử dụng cột kiểu văn bản (TEXThoặcVARCHAR). - Regex được viết cứng (Hard-coded regexes) – Các kế hoạch viễn thông quốc gia luôn thay đổi. Mexico đã bỏ tiền tố đầu số (trunk prefix) vào năm 2019; Argentina hiện yêu cầu thêm số
9sau mã quốc gia đối với các thuê bao di động. Một mẫu (pattern) tĩnh sẽ nhanh chóng trở nên lỗi thời. - Loại bỏ số 0 một cách mù quáng (Blind zero stripping) – Số điện thoại cố định tại Ý giữ số 0 ở đầu, trong khi số của Đức thì không. Một quy tắc chung kiểu “loại bỏ tất cả số 0 ở đầu” sẽ làm hỏng dữ liệu của Ý trong khi không ảnh hưởng gì đến số của Đức.
- Đánh đồng định dạng với khả năng gửi được (Assuming format equals deliverability) –
libphonenumberxác thực cấu trúc nhưng không thể cho biết thiết bị có đang bật hay số đó đã được chuyển mạng hay chưa.
Xây dựng một quy trình (pipeline) đáng tin cậy
- Yêu cầu chọn quốc gia – Thêm bộ chọn quốc gia vào các biểu mẫu đăng ký và truyền vùng đó vào bộ phân tích (parser).
- Hiển thị định dạng trực tiếp – Sử dụng định dạng kiểu “AsYouType” để người dùng thấy được mẫu chính xác ngay khi họ đang nhập.
- Xác thực khi rời khỏi trường nhập (Validate on blur) – Thực hiện xác thực sau khi người dùng rời khỏi trường nhập thay vì thực hiện sau mỗi phím bấm; điều này giúp giảm bớt sự phiền toái.
- Chỉ lưu trữ chuỗi E.164 – Lưu số điện thoại đã được chuẩn hóa có tiền tố
+vào cơ sở dữ liệu. - Định dạng ở lớp hiển thị (Format at the edge) – Chỉ chuyển đổi lại về định dạng thân thiện với người dùng trong giao diện người dùng (UI) hoặc các mẫu email.
Khi di chuyển dữ liệu cũ (legacy data), hãy giữ lại các dòng không vượt qua được bước xác thực. Phân tích từng mục hiện có vào một cột mới, đánh dấu các lỗi và tạo một báo cáo. Việc âm thầm loại bỏ dữ liệu sẽ tạo ra các yêu cầu hỗ trợ (support tickets) mà sau này sẽ trở thành những lỗi tốn kém để khắc phục.
Danh sách kiểm tra nhanh dành cho nhà phát triển
- Lưu trữ số điện thoại dưới dạng
TEXT/VARCHARtheo định dạng E.164. - Sử dụng thư viện libphonenumber của Google; nó xử lý được thực tế phức tạp của các kế hoạch viễn thông toàn cầu.
- Cung cấp vùng mặc định cho những người dùng bỏ qua mã quốc gia.
- Gọi hàm
is_valid_numbercho các lượt đăng ký trực tiếp; hàm này kiểm tra xem số điện thoại có phù hợp với các quy tắc khu vực hay không. - Sử dụng
is_possible_numberkhi làm sạch dữ liệu lớn; nó giúp phát hiện các mục bị sai định dạng rõ rệt mà không từ chối các trường hợp nằm ở ranh giới (borderline cases).
Xử lý số điện thoại đúng cách không phải là một tính năng "có thì tốt"; nó là điều kiện tiên quyết cho bất kỳ hệ thống nào dựa vào việc liên lạc đáng tin cậy với người dùng. Bằng cách chuẩn hóa về E.164 và ủy thác việc phân tích cho một thư viện đã được kiểm chứng, các nhà phát triển có thể loại bỏ một loại lỗi vốn âm thầm làm xói mòn niềm tin và doanh thu. Hãy coi số điện thoại là dữ liệu có cấu trúc, chứ không phải là văn bản tự do, và hãy để các tiêu chuẩn thực hiện những phần việc nặng nhọc nhất.
