提问人:Josh Potter 提问时间:5/17/2015 最后编辑:MachavityJosh Potter 更新时间:11/19/2023 访问量:299712
如何使用PHP的password_hash对密码进行哈希处理和验证
How to use PHP's password_hash to hash and verify passwords
问:
最近,我一直在尝试在我在互联网上偶然发现的登录脚本上实现我自己的安全性。在努力学习如何制作自己的脚本为每个用户生成盐之后,我偶然发现了.password_hash
据我了解(根据本页的阅读),当您使用 .这是真的吗?password_hash
我的另一个问题是,有 2 种盐不是很聪明吗?一个直接在文件中,一个在数据库中?这样,如果有人在数据库中破坏了你的盐,你仍然直接在文件中拥有那个盐?我在这里读到,储存盐从来都不是一个聪明的主意,但它总是让我感到困惑,人们是什么意思。
答:
是的,这是真的。为什么你怀疑函数上的php常见问题?:)
运行的结果有四个部分:password_hash()
- 使用的算法
- 参数
- 盐
- 实际密码哈希
所以正如你所看到的,哈希是其中的一部分。
当然,你可以有一个额外的盐来增加一层安全,但老实说,我认为这在常规的 php 应用程序中是矫枉过正的。默认的 bcrypt 算法很好,可选的河豚算法可以说更好。
评论
是的,您理解正确,函数 password_hash() 将自行生成一个盐,并将其包含在生成的哈希值中。将盐存储在数据库中是绝对正确的,即使已知,它也能完成其工作。
// 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);
您提到的第二个盐(存储在文件中的盐)实际上是 pepper 或服务器端密钥。如果你在散列之前添加它(如盐),那么你就添加了胡椒粉。不过,还有一种更好的方法,您可以先计算哈希值,然后使用服务器端密钥加密(双向)哈希值。这使您可以在必要时更改密钥。
与盐相比,这把钥匙应该保密。人们经常把它混在一起,试图隐藏盐,但最好让盐发挥作用,并用钥匙添加秘密。
使用是存储密码的推荐方法。不要将它们分成数据库和文件。password_hash
假设我们有以下输入:
$password = $_POST['password'];
首先通过执行以下操作对密码进行哈希处理:
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
然后查看输出:
var_dump($hashed_password);
正如你所看到的,它是散列的。(我假设你做了这些步骤)。
现在,将此哈希密码存储在数据库中,确保密码列足够大以保存哈希值(至少 60 个字符或更长)。当用户要求登录时,您可以通过执行以下操作在数据库中检查具有此哈希值的密码输入:
// 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.
评论
PHP的密码函数中内置的向后和向前兼容性明显缺乏讨论。特别是:
- 向后兼容性:密码函数本质上是
crypt()
的一个精心编写的包装器,并且本质上与 -format 哈希向后兼容,即使它们使用过时和/或不安全的哈希算法。crypt()
- 转发兼容性:在身份验证工作流中插入
password_needs_rehash()
和一些逻辑可以使您的哈希值与当前和未来的算法保持同步,而未来对工作流的更改可能为零。注意:任何与指定算法不匹配的字符串都将被标记为需要重新哈希,包括与加密不兼容的哈希。
例如:
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;
}
输出:
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)
最后,鉴于您只能在登录时重新散列用户的密码,您应该考虑“停用”不安全的旧哈希来保护您的用户。我的意思是,在一定的宽限期之后,您可以删除所有不安全的 [例如:裸露的 MD5/SHA/其他弱] 哈希值,并让您的用户依赖应用程序的密码重置机制。
评论
password_hash
cost
password_needs_rehash()
cost
为了使数据库密码在数据库被黑客入侵时更加安全,您可以使用电子邮件地址和密码创建多个列(在我看来,电子邮件地址也应该进行编码),例如:从 email_1 到 email_7 的列和从 password_1 到 password_7 的列。密码可以存储在其中一个电子邮件列中,电子邮件地址可以存储在其中一个密码列中。在剩下的六个中,我们可以存储一个加密的随机字符串。其次,在哈希时,我们可以使用各种哈希函数,例如我们的密码$password,
$password = hash('sha384', $password);
$password = hash('sha256', $password);
$password = hash('sha512', $password);
$password = hash('guest', $password);
$h1 = hash('md5', $password);
$h2 = hash('md5', $h1);
$h3 = hash('md5', $h2);
$h4 = hash('md5', $h3);
$hash_db = $h1 . $h2 . $h3 . $h4;
我们存储在数据库中。哈希值长度为 128 个字符,看起来使用了 sha512 算法。$hash_db
评论