如何防止每次登录密码的bcrypt盐值发生变化

5
我从https://dev59.com/4G445IYBdhLWcg3wkLLU#6337021获取了定义bcrypt函数的代码。密码注册功能正常,将所有字段保存到数据库表中。问题在于密码登录无法正常工作,因为当它被散列并添加盐时,会添加不同的盐。问题出在哪里,我该如何解决?
我尝试过以下方法:
  • 尝试从“new bcrypt”中去掉“new”
  • 在登录和注册脚本中都将$salt添加到hash('$password_login',$salt)
  • 搜索类似于我的情况,但我找到的都是关于比较已散列的$password_login和存储的已散列的$pswd的问题/主题
  • 还添加了echo“$hash”,echo“$isGood”来确定它们是否被验证以及在不访问数据库的情况下它们的样子
index.php中的登录脚本
<?
//Login Script
if (isset($_POST["user_login"]) && isset($_POST["password_login"])) {
    $user_login = (!empty($_POST['user_login'])) ? $_POST['user_login'] : ''; // filter everything but numbers and letters
    $password_login = (!empty($_POST['password_login'])) ? $_POST['password_login'] : ''; // filter everything but numbers and letters 
        $bcrypt = new Bcrypt(10);

        $hash = $bcrypt->hash('$password_login', $salt);
        $isGood = $bcrypt->verify('$password_login', $hash);
        echo "$hash";
        exit();
    $db = new PDO('mysql:host=localhost;dbname=socialnetwork', 'root', 'abc123');
    $db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
    $sql = $db->prepare("SELECT id FROM users WHERE username = :user_login AND password = :password_login LIMIT 1");
    if ($sql->execute(array(
    ':user_login' => $user_login,
    ':password_login' => $hash))) {
        if ($sql->rowCount() > 0){
            while($row = $sql->fetch()){
                $id = $row["id"];
            }
            $_SESSION["id"] = $id;
            $_SESSION["user_login"] = $user_login;
            $_SESSION["password_login"] = $hash;
        } else {
            echo 'Either the password or username you have entered is incorrect. Please check them and try again!';
            exit();
        }
    }
}
?>

在index.php中注册脚本。
<?
class Bcrypt {
  private $rounds;
  public function __construct($rounds = 12) {
    if(CRYPT_BLOWFISH != 1) {
      throw new Exception("bcrypt not supported in this installation. See http://php.net/crypt");
    }

    $this->rounds = $rounds;
  }

  public function hash($input) {
    $hash = crypt($input, $this->getSalt());

    if(strlen($hash) > 13)
      return $hash;

    return false;
  }

  public function verify($input, $existingHash) {
    $hash = crypt($input, $existingHash);

    return $hash === $existingHash;
  }

  private function getSalt() {
    $salt = sprintf('$2a$%02d$', $this->rounds);

    $bytes = $this->getRandomBytes(16);

    $salt .= $this->encodeBytes($bytes);

    return $salt;
  }

  private $randomState;
  private function getRandomBytes($count) {
    $bytes = '';

    if(function_exists('openssl_random_pseudo_bytes') &&
        (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win
      $bytes = openssl_random_pseudo_bytes($count);
    }

    if($bytes === '' && is_readable('/dev/urandom') &&
       ($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) {
      $bytes = fread($hRand, $count);
      fclose($hRand);
    }

    if(strlen($bytes) < $count) {
      $bytes = '';

      if($this->randomState === null) {
        $this->randomState = microtime();
        if(function_exists('getmypid')) {
          $this->randomState .= getmypid();
        }
      }

      for($i = 0; $i < $count; $i += 16) {
        $this->randomState = md5(microtime() . $this->randomState);

        if (PHP_VERSION >= '5') {
          $bytes .= md5($this->randomState, true);
        } else {
          $bytes .= pack('H*', md5($this->randomState));
        }
      }

      $bytes = substr($bytes, 0, $count);
    }

    return $bytes;
  }

  private function encodeBytes($input) {
    // The following is code from the PHP Password Hashing Framework
    $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

    $output = '';
    $i = 0;
    do {
      $c1 = ord($input[$i++]);
      $output .= $itoa64[$c1 >> 2];
      $c1 = ($c1 & 0x03) << 4;
      if ($i >= 16) {
        $output .= $itoa64[$c1];
        break;
      }

      $c2 = ord($input[$i++]);
      $c1 |= $c2 >> 4;
      $output .= $itoa64[$c1];
      $c1 = ($c2 & 0x0f) << 2;

      $c2 = ord($input[$i++]);
      $c1 |= $c2 >> 6;
      $output .= $itoa64[$c1];
      $output .= $itoa64[$c2 & 0x3f];
    } while (1);

    return $output;
  }
}
$reg = @$_POST['reg'];
//declaring variables to prevent errors
//registration form
$fn = (!empty($_POST['fname'])) ? $_POST['fname'] : '';
$ln = (!empty($_POST['lname'])) ? $_POST['lname'] : '';
$un = (!empty($_POST['username'])) ? $_POST['username'] : '';
$em = (!empty($_POST['email'])) ? $_POST['email'] : '';
$em2 = (!empty($_POST['email2'])) ? $_POST['email2'] : '';
$pswd = (!empty($_POST['password'])) ? $_POST['password'] : '';
$pswd2 = (!empty($_POST['password2'])) ? $_POST['password2'] : '';
$d = date("y-m-d"); // Year - Month - Day

if ($reg) {
    if ($em==$em2) {
        // Check if user already exists
        $statement = $db->prepare('SELECT username FROM users WHERE username = :username');
            if ($statement->execute(array(':username' => $un))) {
                if ($statement->rowCount() > 0){
                    //user exists
                    echo "Username already exists, please choose another user name.";
                    exit();
                }
            }
                    //check all of the fields have been filled in
                        if ($fn&&$ln&&$un&&$em&&$em2&&$pswd&&$pswd2) {
                            //check that passwords match
                                if ($pswd==$pswd2) {
                                    //check the maximum length of username/first name/last name does not exceed 25 characters
                                        if (strlen($un)>25||strlen($fn)>25||strlen($ln)>25) {
                                            echo "The maximum limit for username/first name/last name is 25 characters!";
                                        }
                                        else
                                            {
                                                //check the length of the password is between 5 and 30 characters long
                                                    if (strlen($pswd)>30||strlen($pswd)<5) {
                                                        echo "Your password must be between 5 and 30 characters long!";
                                                    }
                                                    else
                                                        {
                                                            //encrypt password and password 2 using md5 before sending to database
                                                                $bcrypt = new Bcrypt(10);

$hash = $bcrypt->hash('$pswd', $salt);
echo "<p>$hash</p>";
$isGood = $bcrypt->verify('$pswd', $hash);
echo "<p>$isGood</p>";

                                                                $bcrypt2 = new Bcrypt(10);

$hash2 = $bcrypt2->hash('$pswd2', $salt);
echo "<p>$hash2</p>";
$isGood2 = $bcrypt2->verify('$pswd2', $hash2);
echo "<p>$isGood2</p>";
exit();                                                             
                                                                $db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
                                                                $sql = 'INSERT INTO users (username, first_name, last_name, email, password, sign_up_date)';
                                                                $sql .= 'VALUES (:username, :first_name, :last_name, :email, :password, :sign_up_date)';

                                                                $query=$db->prepare($sql);

                                                                $query->bindParam(':username', $un, PDO::PARAM_STR);
                                                                $query->bindParam(':first_name', $fn, PDO::PARAM_STR);
                                                                $query->bindParam(':last_name', $ln, PDO::PARAM_STR);
                                                                $query->bindParam(':email', $em, PDO::PARAM_STR);
                                                                $query->bindParam(':password', $hash, PDO::PARAM_STR);
                                                                $query->bindParam(':sign_up_date', $d, PDO::PARAM_STR);

                                                                $query->execute();

                                                                die("<h2>Welcome to Rebel Connect</h2>Login to your account to get started.");
                                                        }
                                            }
                                }
                                else {
                                    echo "Your passwords do not match!";
                                }
                        }
                else
                    {
                        echo "Please fill in all fields!";
                    }
            }
    else {
        echo "Your e-mails don't match!";
    }
}
?>

更新的登录脚本

<?
//Login Script
if (isset($_POST["user_login"]) && isset($_POST["password_login"])) {
    $user_login = (!empty($_POST['user_login'])) ? $_POST['user_login'] : ''; // filter everything but numbers and letters
    $password_login = (!empty($_POST['password_login'])) ? $_POST['password_login'] : ''; // filter everything but numbers and letters 
    $db = new PDO('mysql:host=localhost;dbname=socialnetwork', 'root', 'abc123');
    $db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
    $sql = $db->prepare("SELECT id, password FROM users WHERE username = :user_login LIMIT 1");
    $dbhash = $db->prepare("SELECT password FROM users WHERE username = :user_login LIMIT 1");
    $isGood = $bcrypt->verify('$_POST["password_login"]', $dbhash);
    echo "$isGood";
    exit(); //here for debugging purposes
    if ($sql->execute(array(
    ':user_login' => $user_login,
    ':password_login' => $hash))) {
        if ($sql->rowCount() > 0){
            while($row = $sql->fetch()){
                $id = $row["id"];
            }
            $_SESSION["id"] = $id;
            $_SESSION["user_login"] = $user_login;
            $_SESSION["password_login"] = $hash;
        } else {
            echo 'Either the password or username you have entered is incorrect. Please check them and try again!';
            exit();
        }
    }
}
?>

更新: 我在某种程度上弄清楚了代码放置的位置。我现在遇到的问题是它没有重定向到首页,但我认为这是由于没有刷新页面造成的。最初,在会话开始和用户登录时,我已经设置了自动刷新。当我回家后,我会更新代码以显示所做的更改。如果我重新加载页面,它确实会转到home.php。

你使用的是哪个版本的PHP?如果你使用的是PHP >= 5.5,那么PHP内置了使用Bcrypt哈希密码的功能(请参阅文档http://php.net/password_hash)。如果你能够使用它而不是自己的实现,这可能会让你的生活变得更加轻松。 - Liv
我正在使用5.3.5吗?我确定它是5.3。 - cbenn95
我也遇到过这种情况。我从那个链接获取了脚本。 - samayo
@Liv 我怀疑很多人还没有使用 PHP 5.5 呢 :P 你知道... 因为它还没有发布 - PeeHaa
我会继续发布我尝试过的编辑,并不时更新代码,但我现在还在上高中课程,更不用说我现在才真正开始热衷于编码。 (我正在自学大多数语言的语法和理解,并在需要时从SO获得帮助)。 - cbenn95
1个回答

7

错误的:

$hash = $bcrypt->hash('$password_login', $salt);
$isGood = $bcrypt->verify('$password_login', $hash);

不要向“verify()”提供新的哈希值,而应该提供从数据库中检索到的现有哈希值。
例如:$b->verify('password', '$2a$12$9bAvuaBrMlWmw8oI9flz9e6pjJniKVpRyr9Fz0ScQVwMJA53R3uQO'); 此外,“hash()”函数只接受一个参数,并生成自己的随机盐。
您的登录脚本流程应为:
1.用户提供用户名/密码。 2.SELECT id, password FROM users WHERE username = ? 3.$isGood = $bcrypt->verify($password_login, $hash_from_db); 4. ??? 5.利润!
编辑:
“verify()”/“crypt()”在后台查看的内容,即存储的哈希值中的含义
注意:在base64编码中,每个字符代表6个位。
$ 2a $ 12 $ 9bAvuaBrMl W mw8oI9flz9e6pjJniKVpRyr9Fz0ScQVwMJA53R3uQO
  |    |    |          | |
  |    |    |          | > the rest of the salted, hashed password
  |    |    |          > this character is split, the first 4 bits are part
  |    |    |            of the salt, the last 2 are part of the hash
  |    |    > the salt
  |    > the number of hashing 'rounds' are performed
  > Scheme ID, 2a is blowfish

我只是通过试错来测试:B 谢谢。虽然我有这个想法,但我知道这是重新生成,只是不知道如何停止重新生成。所以在注册脚本中,我需要用$hash_from_db替换$hash吗? - cbenn95
@cbenn95 不是的,需要更改登录脚本。在验证哈希值之前需要运行数据库查询。 - Sammitch
好的,我在你回复之前考虑了我的评论,并以此为基础。我需要做的是在登录脚本中,通过数据库用户名获取密码,并将其与提供的密码登录哈希进行比较,以进行验证。 - cbenn95
@cbenn95 是的。哈希的盐被打包到哈希的前64位中,verify()/crypt()解析出必要的值以重新计算正确的哈希。 - Sammitch
我通过一些试错找到了它需要放置的位置。除非你没事可做,否则不需要再检查代码了。希望我们的帖子能帮助其他遇到这些问题的人 :) 如果可以的话,我会投票支持你的答案,但目前我不能这样做。 - cbenn95

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接