Xác thực (Authentication) giống như người bảo vệ đứng trước cửa ứng dụng của bạn. Mỗi khi có ai đó đăng ký hoặc đăng nhập, hệ thống của bạn phải quyết định xem họ có đúng là người mà họ đang tự nhận hay không. Nếu làm sai, bạn không chỉ đơn thuần là đang sửa lỗi đăng nhập thất bại. Bạn đang mạo hiểm để dữ liệu thực của người dùng bị rò rỉ ra ngoài. Để thực hiện việc này một cách đúng đắn, bạn cần hai công cụ đáng tin cậy: Bcrypt để bảo vệ mật khẩu khi lưu trữ, và JSON Web Tokens để xác minh danh tính trong khi người dùng thao tác trên ứng dụng.

Tại sao lưu trữ văn bản thuần (Plaintext) lại thất bại

Nếu bạn chỉ rút ra được một quy tắc duy nhất từ bài viết này, hãy là quy tắc này: đừng bao giờ lưu trữ mật khẩu dưới dạng văn bản thuần trong cơ sở dữ liệu của bạn. Không quan trọng việc cơ sở dữ liệu của bạn có nằm sau tường lửa hay bạn có tin tưởng mọi kỹ sư trong nhóm hay không. Việc ghi mật khẩu vào các bảng dưới dạng thô giống như việc để chìa khóa nhà dưới thảm chùi chân vậy. Ngay khoảnh khắc ai đó truy cập được vào cơ sở dữ liệu đó — thông qua một API bị cấu hình sai, một bản sao lưu bị rò rỉ, hoặc một cuộc tấn công injection — mọi thông tin đăng nhập sẽ bị lộ ngay lập tức.

Thiệt hại sẽ nhân lên gấp bội vì mọi người thường tái sử dụng mật khẩu. Một vụ vi phạm duy nhất có thể làm tổn hại không chỉ ứng dụng của bạn, mà còn cả email, tài khoản ngân hàng và mạng xã hội của người dùng. Đó là lý do tại sao chúng ta băm (hash) mật khẩu. Hashing chuyển đổi mật khẩu thành một chuỗi ký tự xáo trộn không có sự tương đồng rõ rệt nào với mật khẩu gốc. Quá trình này là một chiều và không thể đảo ngược. Bạn không thể đổ "axit toán học" lên một mã hash để hòa tan nó trở lại thành mật khẩu ban đầu được.

Băm mật khẩu với Bcrypt

Bcrypt là một hàm băm được xây dựng dành riêng cho mật khẩu. Nó nhận chuỗi văn bản thuần, chạy qua mã hóa Blowfish, và trả về một kết quả trông giống như $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy. Phần tiền tố đó cho bạn biết thuật toán và hệ số chi phí (cost factor). Phần đuôi dài là sự kết hợp của salt và chính mã hash đó.

Salt là dữ liệu ngẫu nhiên được trộn vào mật khẩu trước khi quá trình băm bắt đầu. Vì salt là duy nhất cho mỗi người dùng, hai người cùng chọn "password123" sẽ có kết quả là hai mã hash hoàn toàn khác nhau trong cơ sở dữ liệu của bạn. Sự khác biệt nhỏ nhoi này sẽ vô hiệu hóa các rainbow tables — các từ điển mã hash được tính toán trước cho các mật khẩu phổ biến — bởi vì kẻ tấn công sẽ cần phải tạo lại toàn bộ bảng cho mỗi salt duy nhất.

Bcrypt cũng được cố tình thiết kế để chạy chậm. Phần cứng hiện đại có thể đoán hàng tỷ mã hash mỗi giây với các thuật toán nhanh như SHA-256. Bcrypt thì lại "lê bước" chậm chạp. Nó chạy quá trình băm nhiều lần, được kiểm soát bởi một hệ số chi phí mà bạn có thể tăng lên khi máy tính trở nên nhanh hơn. Sự chậm chạp đó sẽ trừng phạt bất kỳ ai đang cố gắng tấn công brute-force vào một cơ sở dữ liệu bị đánh cắp. Một người dùng hợp lệ chờ thêm hai trăm mili giây khi đăng nhập sẽ không nhận ra điều gì. Nhưng một kẻ tấn công đang cố gắng thử hàng triệu lần đoán thì chắc chắn sẽ nhận thấy.

Cách thức hoạt động của JSON Web Tokens

Trong khi Bcrypt xử lý "cửa chính", thì JWT xử lý "thẻ thông hành" trong hành lang. Một JSON Web Token là một chuỗi ký tự nhỏ gọn, an toàn với URL, dùng để chứng minh rằng người dùng đã được xác thực. Hãy coi nó như một thẻ căn cước kỹ thuật số mà máy chủ cấp và máy khách mang theo bên mình.

Một JWT chứa ba phần được ngăn cách bởi các dấu chấm: header, payload, và signature. Header chỉ định loại token và thuật toán ký. Payload chứa các claims — các tuyên bố về người dùng và chính token đó — chẳng hạn như ID người dùng, tên đăng nhập và dấu thời gian hết hạn. Signature là con dấu mã hóa. Máy chủ tạo ra nó bằng cách mã hóa header và payload, sau đó chạy chúng qua một khóa bí mật (secret key). Nếu bất kỳ ai can thiệp vào payload, signature sẽ không còn khớp nữa, và máy chủ sẽ từ chối token đó ngay lập tức.