Phiên bản trước đó của câu trả lời được chấp nhận ( md5(uniqid(mt_rand(), true))
) là không an toàn và chỉ cung cấp khoảng 2 ^ 60 kết quả đầu ra có thể có - nằm trong phạm vi của tìm kiếm bạo lực trong khoảng thời gian một tuần đối với kẻ tấn công ngân sách thấp:
Vì khóa DES 56 bit có thể bị cưỡng bức trong khoảng 24 giờ và một trường hợp trung bình sẽ có khoảng 59 bit entropy, chúng ta có thể tính 2 ^ 59/2 ^ 56 = khoảng 8 ngày. Tùy thuộc vào cách xác minh mã thông báo này được triển khai, thực tế có thể bị rò rỉ thông tin thời gian và suy ra N byte đầu tiên của mã thông báo đặt lại hợp lệ .
Vì câu hỏi là về "các phương pháp hay nhất" và mở đầu bằng ...
Tôi muốn tạo mã định danh cho trường hợp quên mật khẩu
... chúng ta có thể suy ra rằng mã thông báo này có các yêu cầu bảo mật ngầm. Và khi bạn thêm các yêu cầu bảo mật vào trình tạo số ngẫu nhiên, cách tốt nhất là luôn sử dụng trình tạo số giả ngẫu nhiên an toàn bằng mật mã (viết tắt là CSPRNG).
Sử dụng CSPRNG
Trong PHP 7, bạn có thể sử dụng bin2hex(random_bytes($n))
(ở đâu$n
là một số nguyên lớn hơn 15).
Trong PHP 5, bạn có thể sử dụng random_compat
để hiển thị cùng một API.
Ngoài ra, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
nếu bạn đã ext/mcrypt
cài đặt. Một lớp lót tốt khác là bin2hex(openssl_random_pseudo_bytes($n))
.
Tách Tra cứu khỏi Trình xác thực
Lấy từ công việc trước đây của tôi về cookie "nhớ tôi" an toàn trong PHP , cách hiệu quả duy nhất để giảm thiểu rò rỉ thời gian nói trên (thường được giới thiệu bởi truy vấn cơ sở dữ liệu) là tách việc tra cứu khỏi xác thực.
Nếu bảng của bạn trông giống như thế này (MySQL) ...
CREATE TABLE account_recovery (
id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT
userid INTEGER(11) UNSIGNED NOT NULL,
token CHAR(64),
expires DATETIME,
PRIMARY KEY(id)
);
... bạn cần thêm một cột nữa selector
, như sau:
CREATE TABLE account_recovery (
id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT
userid INTEGER(11) UNSIGNED NOT NULL,
selector CHAR(16),
token CHAR(64),
expires DATETIME,
PRIMARY KEY(id),
KEY(selector)
);
Sử dụng CSPRNG Khi mã thông báo đặt lại mật khẩu được phát hành, hãy gửi cả hai giá trị cho người dùng, lưu trữ bộ chọn và hàm băm SHA-256 của mã thông báo ngẫu nhiên trong cơ sở dữ liệu. Sử dụng bộ chọn để lấy mã băm và ID người dùng, tính toán mã băm SHA-256 của mã thông báo mà người dùng cung cấp với mã được lưu trữ trong cơ sở dữ liệu bằng cách sử dụng hash_equals()
.
Mã mẫu
Tạo mã thông báo đặt lại trong PHP 7 (hoặc 5.6 với random_compat) với PDO:
$selector = bin2hex(random_bytes(8));
$token = random_bytes(32);
$urlToEmail = 'http://example.com/reset.php?'.http_build_query([
'selector' => $selector,
'validator' => bin2hex($token)
]);
$expires = new DateTime('NOW');
$expires->add(new DateInterval('PT01H')); // 1 hour
$stmt = $pdo->prepare("INSERT INTO account_recovery (userid, selector, token, expires) VALUES (:userid, :selector, :token, :expires);");
$stmt->execute([
'userid' => $userId, // define this elsewhere!
'selector' => $selector,
'token' => hash('sha256', $token),
'expires' => $expires->format('Y-m-d\TH:i:s')
]);
Xác minh mã thông báo đặt lại do người dùng cung cấp:
$stmt = $pdo->prepare("SELECT * FROM account_recovery WHERE selector = ? AND expires >= NOW()");
$stmt->execute([$selector]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($results)) {
$calc = hash('sha256', hex2bin($validator));
if (hash_equals($calc, $results[0]['token'])) {
// The reset token is valid. Authenticate the user.
}
// Remove the token from the DB regardless of success or failure.
}
Các đoạn mã này không phải là giải pháp hoàn chỉnh (tôi đã tránh xác thực đầu vào và tích hợp khung), nhưng chúng sẽ đóng vai trò là một ví dụ về những việc cần làm.