Kiểm tra mật khẩu
Thuộc phần Xác thực và bảo mật trong hành trình Node.js của Coddy. Bài học 6 trên 24.
Mỗi lần đăng nhập, hệ thống tạo một khóa từ mật khẩu được nhập, dùng salt đã lưu và độ dài khóa ban đầu. Salt mới sẽ tạo ra khóa khác, ngay cả khi mật khẩu nhập đúng. Hệ thống không bao giờ giải mã mật khẩu gốc từ dữ liệu đã lưu.
// Mã mời được lưu dưới dạng salt.keyHex
const [salt, keyHex] = savedInvite.split('.');
const expected = Buffer.from(keyHex, 'hex');
const attempted = await scryptAsync(inviteCode, salt, 32);So sánh các chuỗi byte thu được bằng timingSafeEqual sau khi xác nhận độ dài của chúng khớp nhau. Cách này tránh thoát sớm dựa trên nội dung, vốn có thể làm lộ thông tin qua thời gian xử lý. === của Buffer kiểm tra danh tính, còn phép so sánh chuỗi thông thường không đảm bảo thời gian xử lý không đổi. Hàm xử lý mật khẩu của bạn dùng dấu phân cách dấu hai chấm trong bản ghi mật khẩu và khóa dài 64 byte. Tuyến POST /login mà bạn thêm vào dùng kết quả để trả về mã 200 hoặc 401.
Thử thách
DễBước này mở dự án API ghi chú riêng tư đã chuẩn bị sẵn của khóa học, chứ không phải các tệp dự án từ Never Store Passwords của bạn (bao gồm passwords.js). Phần bạn đã làm trước đó vẫn nằm trong bài học riêng. Hãy đọc các tệp được cung cấp trước khi chỉnh sửa passwords.js. Bản sao này có các hàm trợ giúp băm mật khẩu và những route gỡ lỗi được cung cấp sẵn.
Giờ đây, đăng ký lưu các giá trị băm có salt. Hãy thêm chức năng đăng nhập để kiểm tra mật khẩu với giá trị đã lưu khi đăng ký.
- Trong
passwords.js, hãy thêm và export một hàmasyncverifyPassword(password, stored).storedlà giá trị đã lưu khi đăng ký, chẳng hạnf2bb...f065:c3f7...dbd2. Trả vềtruenếupasswordlà mật khẩu đã tạo ra giá trị đó, nếu không thì trả vềfalse:- Lấy salt ở trước dấu hai chấm.
- Băm
passwordvới salt đó thành khóa 64 byte, theo cách tương tự nhưhashPassword. - Giải mã khóa đã lưu sau dấu hai chấm từ hệ thập lục phân. Nếu hai khóa có độ dài byte khác nhau, hãy trả về
falsemà không gọitimingSafeEqual, như trong bài Comparing Key Bytes. Nếu không, hãy trả về kết quả so sánh.
- Trong
server.js, hãy importverifyPasswordbên cạnhhashPassword. ThêmPOST /login. Route này nhận cùng nội dung body như khi đăng ký, chẳng hạn{"username": "ada", "password": "correct horse"}.- Tìm người dùng có username đó.
verifyPasswordchạy chậm và trả về một promise, vì vậy hãy chờ hàm này hoàn tất. - Nếu người dùng tồn tại và mật khẩu chính xác, hãy phản hồi với mã trạng thái 200 và
{"username": "ada"}. - Nếu không, hãy phản hồi với mã trạng thái 401 và
{"error": "Wrong username or password"}. 401 Unauthorized nghĩa là yêu cầu chưa chứng minh được danh tính của người gửi. - Gửi cùng một phản hồi 401 cho cả trường hợp mật khẩu sai lẫn trường hợp chưa có ai đăng ký username đó. Như vậy, nội dung phản hồi sẽ không tiết lộ username nào tồn tại.
- Tìm người dùng có username đó.
Thử nhé: Nhấn Run. Gửi POST /signup với {"username": "ada", "password": "correct horse"}: mã trạng thái 201. Gửi POST /login với cùng nội dung body: mã trạng thái 200 và {"username": "ada"}. Gửi POST /login với {"username": "ada", "password": "wrong horse"}: mã trạng thái 401 và {"error": "Wrong username or password"}. Gửi POST /login với {"username": "nobody", "password": "correct horse"}: cùng mã trạng thái 401.
Tự làm thử
import { randomBytes, scrypt } from 'node:crypto';
import { promisify } from 'node:util';
const scryptAsync = promisify(scrypt);
// Chuyển mật khẩu thành chuỗi mà quá trình đăng ký lưu lại.
export async function hashPassword(password) {
const salt = randomBytes(16).toString('hex');
const key = await scryptAsync(password, salt, 64);
return `${salt}:${key.toString('hex')}`;
}
Tất cả bài học trong Xác thực và bảo mật
Tự luyện tập: Playground Express