Cách sử dụng password_hash của PHP để băm và xác minh mật khẩu


94

Gần đây, tôi đã cố gắng thực hiện bảo mật của riêng mình trên một tập lệnh đăng nhập mà tôi tình cờ tìm thấy trên internet. Sau khi cố gắng tìm hiểu cách tạo tập lệnh của riêng mình để tạo ra muối cho mỗi người dùng, tôi đã vấp phải password_hash.

Theo những gì tôi hiểu (dựa trên bài đọc trên trang này ), muối đã được tạo trong hàng khi bạn sử dụng password_hash. Điều này có đúng không?

Một câu hỏi khác của tôi là, liệu có thông minh nếu có 2 muối không? Một trực tiếp trong tệp và một trong DB? Bằng cách đó, nếu ai đó xâm phạm muối của bạn trong DB, bạn vẫn có một người trực tiếp trong tệp? Tôi đã đọc ở đây rằng lưu trữ muối không bao giờ là một ý tưởng thông minh, nhưng nó luôn khiến tôi bối rối về ý nghĩa của mọi người.


8
Không. Hãy để chức năng chăm sóc muối. Việc ướp muối hai lần sẽ khiến bạn gặp rắc rối và không cần thiết.
Funk Forty Niner

Câu trả lời:


182

Sử dụng password_hashlà cách được khuyến khích để lưu trữ mật khẩu. Đừng tách chúng thành DB và tệp.

Giả sử chúng ta có dữ liệu đầu vào sau:

$password = $_POST['password'];

Trước tiên, bạn băm mật khẩu bằng cách thực hiện:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

Sau đó xem kết quả:

var_dump($hashed_password);

Như bạn có thể thấy, nó đã được băm. (Tôi giả sử bạn đã làm những bước đó).

Bây giờ bạn lưu trữ mật khẩu đã băm này trong cơ sở dữ liệu của mình, đảm bảo cột mật khẩu của bạn đủ lớn để chứa giá trị băm (ít nhất 60 ký tự trở lên) . Khi người dùng yêu cầu đăng nhập họ, bạn kiểm tra đầu vào mật khẩu với giá trị băm này trong cơ sở dữ liệu, bằng cách thực hiện như sau:

// Query the database for username and password
// ...

if(password_verify($password, $hashed_password)) {
    // If the password inputs matched the hashed password in the database
    // Do something, you know... log them in.
} 

// Else, Redirect them back to the login page.

Tham khảo chính thức


2
Ok, tôi vừa thử cái này và nó hoạt động. Tôi nghi ngờ chức năng này vì nó dường như quá dễ dàng. Bạn khuyên tôi nên tạo độ dài cho varchar của mình trong bao lâu? 225?
Josh Potter

4
Điều này đã có trong hướng dẫn sử dụng php.net/manual/en/ Chức năng.password-hash.php --- php.net/manual/en/ Chức năng.password-verify.php mà OP có thể không đọc hoặc không hiểu. Câu hỏi này được hỏi thường xuyên hơn không.
Funk Forty Niner

Đó là một trang khác.
Josh Potter

@JoshPotter khác gì? thêm vào đó, nhận thấy rằng họ chưa trả lời câu hỏi thứ hai của bạn. có thể họ đang mong bạn tự tìm hiểu hoặc họ không biết.
Funk Forty Niner

8
@FunkFortyNiner, b / c Josh đã đặt câu hỏi, tôi đã tìm thấy nó, 2 năm sau và nó đã giúp tôi. Đó là điểm của SO. Sách hướng dẫn đó rõ ràng như bùn.
toddmo

23

Vâng, bạn đã hiểu đúng, hàm password_hash () sẽ tự tạo ra một muối và đưa nó vào giá trị băm kết quả. Lưu trữ muối trong cơ sở dữ liệu là hoàn toàn chính xác, nó thực hiện công việc của mình ngay cả khi đã biết.

// Hash a new password for storing in the database.
// The function automatically generates a cryptographically safe salt.
$hashToStoreInDb = password_hash($_POST['password'], PASSWORD_DEFAULT);

// Check if the hash of the entered login password, matches the stored hash.
// The salt and the cost factor will be extracted from $existingHashFromDb.
$isPasswordCorrect = password_verify($_POST['password'], $existingHashFromDb);

Muối thứ hai mà bạn đã đề cập (muối được lưu trữ trong tệp), thực sự là hạt tiêu hoặc khóa phía máy chủ. Nếu bạn thêm nó trước khi băm (như muối), sau đó bạn thêm một hạt tiêu. Tuy nhiên, có một cách tốt hơn, trước tiên bạn có thể tính toán hàm băm và sau đó mã hóa (hai chiều) hàm băm bằng khóa phía máy chủ. Điều này cho phép bạn thay đổi chìa khóa khi cần thiết.

Ngược lại với muối, chìa khóa này nên được giữ bí mật. Mọi người thường trộn nó lên và cố gắng giấu muối, nhưng tốt hơn là để cho muối làm nhiệm vụ của nó và thêm bí quyết bằng chìa khóa.


8

Vâng đúng vậy. Tại sao bạn nghi ngờ câu hỏi thường gặp về php trên chức năng? :)

Kết quả của việc chạy password_hash()có bốn phần:

  1. thuật toán được sử dụng
  2. thông số
  3. Muối
  4. mật khẩu thực tế băm

Vì vậy, như bạn có thể thấy, băm là một phần của nó.

Chắc chắn, bạn có thể có thêm muối để tăng thêm lớp bảo mật, nhưng tôi thành thật nghĩ rằng điều đó là quá mức cần thiết trong một ứng dụng php thông thường. Thuật toán bcrypt mặc định là tốt, và thuật toán Blowfish tùy chọn thậm chí còn tốt hơn.


2
BCrypt là một hàm băm , trong khi Blowfish là một thuật toán để mã hóa . BCrypt bắt nguồn từ thuật toán Blowfish.
martinstoeckli

7

Không bao giờ sử dụng md5 () để bảo mật mật khẩu của bạn, ngay cả với muối, nó luôn nguy hiểm !!

Đảm bảo mật khẩu của bạn được bảo mật bằng các thuật toán băm mới nhất như bên dưới.

<?php

// Your original Password
$password = '121@121';

//PASSWORD_BCRYPT or PASSWORD_DEFAULT use any in the 2nd parameter
/*
PASSWORD_BCRYPT always results 60 characters long string.
PASSWORD_DEFAULT capacity is beyond 60 characters
*/
$password_encrypted = password_hash($password, PASSWORD_BCRYPT);

Để khớp với mật khẩu được mã hóa của cơ sở dữ liệu và mật khẩu do người dùng nhập, hãy sử dụng chức năng bên dưới.

<?php 

if (password_verify($password_inputted_by_user, $password_encrypted)) {
    // Success!
    echo 'Password Matches';
}else {
    // Invalid credentials
    echo 'Password Mismatch';
}

Nếu bạn muốn sử dụng muối của riêng mình, hãy sử dụng hàm được tạo tùy chỉnh của bạn cho tương tự, chỉ cần làm theo bên dưới, nhưng tôi không khuyến khích điều này vì Nó được tìm thấy không được dùng trong các phiên bản PHP mới nhất.

Đọc về password_hash () trước khi sử dụng mã bên dưới.

<?php

$options = [
    'salt' => your_custom_function_for_salt(), 
    //write your own code to generate a suitable & secured salt
    'cost' => 12 // the default cost is 10
];

$hash = password_hash($your_password, PASSWORD_DEFAULT, $options);

4
Tùy chọn muối không được chấp nhận vì những lý do chính đáng, vì hàm làm hết sức mình để tạo ra một muối an toàn về mật mã và gần như không thể làm điều đó tốt hơn.
martinstoeckli

@martinstoeckli, vâng bạn nói đúng, tôi vừa cập nhật câu trả lời của mình, Cảm ơn!
Mahesh Yadav

if (Isset ($ _ POST ['btn-signup'])) {$ uname = mysql_real_escape_string ($ _ POST ['uname']); $ email = mysql_real_escape_string ($ _ POST ['email']); $ upass = md5 (mysql_real_escape_string ($ _ POST ['pass'])); Đây là mã được sử dụng trong login.php .. tôi muốn làm mà không sử dụng thoát và md5. tôi muốn sử dụng hàm băm mật khẩu ..
rabmi sm

PASSWORD_DEFAULT - Sử dụng thuật toán bcrypt (Yêu cầu PHP 5.5.0). Lưu ý rằng hằng số này được thiết kế để thay đổi theo thời gian khi các thuật toán mới và mạnh hơn được thêm vào PHP. Vì lý do đó, độ dài của kết quả từ việc sử dụng số nhận dạng này có thể thay đổi theo thời gian.
Adrian P.

5

Có sự thiếu thảo luận rõ ràng về khả năng tương thích ngược và chuyển tiếp được tích hợp trong các hàm mật khẩu của PHP. Đáng chú ý:

  1. Tương thích ngược: Các hàm mật khẩu về cơ bản là một trình bao bọc được viết tốt xung quanh crypt()và vốn dĩ tương thích ngược với các hàm crypt()băm-định dạng, ngay cả khi chúng sử dụng các thuật toán băm lỗi thời và / hoặc không an toàn.
  2. Forwards Compatibilty: Chèn password_needs_rehash()và một chút logic vào quy trình xác thực của bạn có thể giúp bạn cập nhật mã băm của mình với các thuật toán hiện tại và tương lai mà không có thay đổi nào trong tương lai đối với quy trình làm việc. Lưu ý: Bất kỳ chuỗi nào không khớp với thuật toán đã chỉ định sẽ bị gắn cờ vì cần xử lý lại, bao gồm cả các hàm băm không tương thích với mật mã.

Ví dụ:

class FakeDB {
    public function __call($name, $args) {
        printf("%s::%s(%s)\n", __CLASS__, $name, json_encode($args));
        return $this;
    }
}

class MyAuth {
    protected $dbh;
    protected $fakeUsers = [
        // old crypt-md5 format
        1 => ['password' => '$1$AVbfJOzY$oIHHCHlD76Aw1xmjfTpm5.'],
        // old salted md5 format
        2 => ['password' => '3858f62230ac3c915f300c664312c63f', 'salt' => 'bar'],
        // current bcrypt format
        3 => ['password' => '$2y$10$3eUn9Rnf04DR.aj8R3WbHuBO9EdoceH9uKf6vMiD7tz766rMNOyTO']
    ];

    public function __construct($dbh) {
        $this->dbh = $dbh;
    }

    protected function getuser($id) {
        // just pretend these are coming from the DB
        return $this->fakeUsers[$id];
    }

    public function authUser($id, $password) {
        $userInfo = $this->getUser($id);

        // Do you have old, turbo-legacy, non-crypt hashes?
        if( strpos( $userInfo['password'], '$' ) !== 0 ) {
            printf("%s::legacy_hash\n", __METHOD__);
            $res = $userInfo['password'] === md5($password . $userInfo['salt']);
        } else {
            printf("%s::password_verify\n", __METHOD__);
            $res = password_verify($password, $userInfo['password']);
        }

        // once we've passed validation we can check if the hash needs updating.
        if( $res && password_needs_rehash($userInfo['password'], PASSWORD_DEFAULT) ) {
            printf("%s::rehash\n", __METHOD__);
            $stmt = $this->dbh->prepare('UPDATE users SET pass = ? WHERE user_id = ?');
            $stmt->execute([password_hash($password, PASSWORD_DEFAULT), $id]);
        }

        return $res;
    }
}

$auth = new MyAuth(new FakeDB());

for( $i=1; $i<=3; $i++) {
    var_dump($auth->authuser($i, 'foo'));
    echo PHP_EOL;
}

Đầu ra:

MyAuth::authUser::password_verify
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["$2y$10$zNjPwqQX\/RxjHiwkeUEzwOpkucNw49yN4jjiRY70viZpAx5x69kv.",1]])
bool(true)

MyAuth::authUser::legacy_hash
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["$2y$10$VRTu4pgIkGUvilTDRTXYeOQSEYqe2GjsPoWvDUeYdV2x\/\/StjZYHu",2]])
bool(true)

MyAuth::authUser::password_verify
bool(true)

Lưu ý cuối cùng, do bạn chỉ có thể băm lại mật khẩu của người dùng khi đăng nhập, bạn nên cân nhắc việc "hủy bỏ" các hàm băm cũ không an toàn để bảo vệ người dùng của bạn. Bởi điều này, ý tôi là sau một khoảng thời gian gia hạn nhất định, bạn xóa tất cả các băm không an toàn [ví dụ: bare MD5 / SHA / nếu không thì yếu] và yêu cầu người dùng dựa vào cơ chế đặt lại mật khẩu của ứng dụng của bạn.


0

Mật khẩu lớp đầy đủ mã:

Class Password {

    public function __construct() {}


    /**
     * Hash the password using the specified algorithm
     *
     * @param string $password The password to hash
     * @param int    $algo     The algorithm to use (Defined by PASSWORD_* constants)
     * @param array  $options  The options for the algorithm to use
     *
     * @return string|false The hashed password, or false on error.
     */
    function password_hash($password, $algo, array $options = array()) {
        if (!function_exists('crypt')) {
            trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
            return null;
        }
        if (!is_string($password)) {
            trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
            return null;
        }
        if (!is_int($algo)) {
            trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
            return null;
        }
        switch ($algo) {
            case PASSWORD_BCRYPT :
                // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
                $cost = 10;
                if (isset($options['cost'])) {
                    $cost = $options['cost'];
                    if ($cost < 4 || $cost > 31) {
                        trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
                        return null;
                    }
                }
                // The length of salt to generate
                $raw_salt_len = 16;
                // The length required in the final serialization
                $required_salt_len = 22;
                $hash_format = sprintf("$2y$%02d$", $cost);
                break;
            default :
                trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
                return null;
        }
        if (isset($options['salt'])) {
            switch (gettype($options['salt'])) {
                case 'NULL' :
                case 'boolean' :
                case 'integer' :
                case 'double' :
                case 'string' :
                    $salt = (string)$options['salt'];
                    break;
                case 'object' :
                    if (method_exists($options['salt'], '__tostring')) {
                        $salt = (string)$options['salt'];
                        break;
                    }
                case 'array' :
                case 'resource' :
                default :
                    trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
                    return null;
            }
            if (strlen($salt) < $required_salt_len) {
                trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
                return null;
            } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
                $salt = str_replace('+', '.', base64_encode($salt));
            }
        } else {
            $salt = str_replace('+', '.', base64_encode($this->generate_entropy($required_salt_len)));
        }
        $salt = substr($salt, 0, $required_salt_len);

        $hash = $hash_format . $salt;

        $ret = crypt($password, $hash);

        if (!is_string($ret) || strlen($ret) <= 13) {
            return false;
        }

        return $ret;
    }


    /**
     * Generates Entropy using the safest available method, falling back to less preferred methods depending on support
     *
     * @param int $bytes
     *
     * @return string Returns raw bytes
     */
    function generate_entropy($bytes){
        $buffer = '';
        $buffer_valid = false;
        if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
            $buffer = mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
            $buffer = openssl_random_pseudo_bytes($bytes);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid && is_readable('/dev/urandom')) {
            $f = fopen('/dev/urandom', 'r');
            $read = strlen($buffer);
            while ($read < $bytes) {
                $buffer .= fread($f, $bytes - $read);
                $read = strlen($buffer);
            }
            fclose($f);
            if ($read >= $bytes) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid || strlen($buffer) < $bytes) {
            $bl = strlen($buffer);
            for ($i = 0; $i < $bytes; $i++) {
                if ($i < $bl) {
                    $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
                } else {
                    $buffer .= chr(mt_rand(0, 255));
                }
            }
        }
        return $buffer;
    }

    /**
     * Get information about the password hash. Returns an array of the information
     * that was used to generate the password hash.
     *
     * array(
     *    'algo' => 1,
     *    'algoName' => 'bcrypt',
     *    'options' => array(
     *        'cost' => 10,
     *    ),
     * )
     *
     * @param string $hash The password hash to extract info from
     *
     * @return array The array of information about the hash.
     */
    function password_get_info($hash) {
        $return = array('algo' => 0, 'algoName' => 'unknown', 'options' => array(), );
        if (substr($hash, 0, 4) == '$2y$' && strlen($hash) == 60) {
            $return['algo'] = PASSWORD_BCRYPT;
            $return['algoName'] = 'bcrypt';
            list($cost) = sscanf($hash, "$2y$%d$");
            $return['options']['cost'] = $cost;
        }
        return $return;
    }

    /**
     * Determine if the password hash needs to be rehashed according to the options provided
     *
     * If the answer is true, after validating the password using password_verify, rehash it.
     *
     * @param string $hash    The hash to test
     * @param int    $algo    The algorithm used for new password hashes
     * @param array  $options The options array passed to password_hash
     *
     * @return boolean True if the password needs to be rehashed.
     */
    function password_needs_rehash($hash, $algo, array $options = array()) {
        $info = password_get_info($hash);
        if ($info['algo'] != $algo) {
            return true;
        }
        switch ($algo) {
            case PASSWORD_BCRYPT :
                $cost = isset($options['cost']) ? $options['cost'] : 10;
                if ($cost != $info['options']['cost']) {
                    return true;
                }
                break;
        }
        return false;
    }

    /**
     * Verify a password against a hash using a timing attack resistant approach
     *
     * @param string $password The password to verify
     * @param string $hash     The hash to verify against
     *
     * @return boolean If the password matches the hash
     */
    public function password_verify($password, $hash) {
        if (!function_exists('crypt')) {
            trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
            return false;
        }
        $ret = crypt($password, $hash);
        if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
            return false;
        }

        $status = 0;
        for ($i = 0; $i < strlen($ret); $i++) {
            $status |= (ord($ret[$i]) ^ ord($hash[$i]));
        }

        return $status === 0;
    }

}

0

Tôi đã xây dựng một chức năng mà tôi luôn sử dụng để xác thực mật khẩu và tạo mật khẩu, ví dụ như để lưu trữ chúng trong cơ sở dữ liệu MySQL. Nó sử dụng một loại muối được tạo ngẫu nhiên sẽ an toàn hơn so với việc sử dụng một loại muối tĩnh.

function secure_password($user_pwd, $multi) {

/*
    secure_password ( string $user_pwd, boolean/string $multi ) 

    *** Description: 
        This function verifies a password against a (database-) stored password's hash or
        returns $hash for a given password if $multi is set to either true or false

    *** Examples:
        // To check a password against its hash
        if(secure_password($user_password, $row['user_password'])) {
            login_function();
        } 
        // To create a password-hash
        $my_password = 'uber_sEcUrE_pass';
        $hash = secure_password($my_password, true);
        echo $hash;
*/

// Set options for encryption and build unique random hash
$crypt_options = ['cost' => 11, 'salt' => mcrypt_create_iv(22, MCRYPT_DEV_URANDOM)];
$hash = password_hash($user_pwd, PASSWORD_BCRYPT, $crypt_options);

// If $multi is not boolean check password and return validation state true/false
if($multi!==true && $multi!==false) {
    if (password_verify($user_pwd, $table_pwd = $multi)) {
        return true; // valid password
    } else {
        return false; // invalid password
    }
// If $multi is boolean return $hash
} else return $hash;

}

6
Tốt nhất là bỏ qua salttham số, nó sẽ được tự động tạo bởi hàm password_hash () , theo các phương pháp hay nhất. Thay vì PASSWORD_BCRYPTngười ta có thể sử dụng PASSWORD_DEFAULTđể viết mã bằng chứng trong tương lai.
martinstoeckli

Cảm ơn vì lời khuyên đó. Tôi phải giám sát nó trong tài liệu. Đó là một số đêm dài.
Gerrit Fries

1
Theo secure.php.net/manual/en/ Chức năng.password-hash.php "Tùy chọn muối đã không được chấp nhận kể từ phiên bản PHP 7.0.0. Hiện tại, bạn chỉ cần sử dụng muối được tạo theo mặc định".
jmng
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.