php - PHP; 如何将受密码保护的登录信息安全地存储为 cookie?
问题描述
这是我的PHP
代码。
<?php
$username = "admin";
$password = "admin";
$session = $_COOKIE['session'];
$private_key = "!$//%$$//%$&=§$!&%&=§$!&%";
if(isset($_POST['login'])) {
if($_POST['username'] == $username && $_POST['password'] == $password) {
setcookie("username", $username, time()+(10*365*24*60*60));
setcookie("session", md5($password.$private_key), time()+(10*365*24*60*60));
echo "You are are logged in!";
} else {
echo "Wrong login!";
}
}
if(isset($_COOKIE['session'])) {
if($_COOKIE['username'] == $username && $_COOKIE['session'] == md5($password.$private_key)) {
echo "You are are logged in!";
} else {
echo "Wrong login!";
}
}
?>
<form method="post" action="">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" name="login">
</form>
此代码的作用:当您使用正确的数据登录时,将使用用户名和散列密码设置 cookie。用户名不是秘密,可以明文存储。密码在散列之前与一个神秘的字符串结合在一起,以防止有人猜到密码。不知道他就不会成功$private_key
。
重新访问该页面时,由于 cookie,您已经登录。
当然,我的剧本并不完美,但是:这是正确的方式吗?
如果没有正确的登录数据,您将无法登录。黑客也无法找到密码,因为它与一个神秘的字符串结合在一起。
但是黑客可以通过某种方式读取 cookie 数据,并且只能通过在浏览器中操纵 cookie 数据来使用 cookie 数据登录。我该如何预防?
解决方案
如果您尝试实现“在这台计算机上记住我”功能(即会话之上的持久身份验证),您将进入一个名副其实的复杂兔子洞。我建议阅读此主题的专用指南。
总结策略:不要将密码存储在 cookie 中(散列或其他)。相反,您将使用随机生成的令牌。具体来说,一个拆分令牌。
长期身份验证(“跨会话记住我”)Cookie
<?php
class Authentication
{
/** @var PDO $db */
private $db;
public function __construct(PDO $pdo)
{
$this->db = $pdo;
}
public function createLongTermToken(int $userId = 0): string
{
// Build the components
$tokenLeft = base64_encode(random_bytes(15));
$tokenRight = base64_encode(random_bytes(33));
$tokenRightHashed = hash('sha256', $tokenRight);
// Insert into the database
$stmt = $this->db->prepare(
"INSERT INTO auth_tokens (user_id, selector, hash) VALUES (?, ?, ?)"
);
$stmt->execute([$userId, $tokenLeft, $tokenRightHashed]);
return $tokenLeft . ':' . $tokenRight;
}
public function loginWithPersistentCookie(string $cookieValue): int
{
// Input validation
if (strpos(':', $cookieValue) === false) {
throw new Exception('Invalid authentication token');
}
list($tokenLeft, $tokenRight) = explode(':', $cookieValue);
if (strlen($tokenLeft) !== 20) || strlen($tokenRight) !== 44) {
throw new Exception('Invalid authentication token');
}
$tokenRightHashed = hash('sha256', $tokenRight);
// Fetch from database
$stmt = $this->db->prepare("SELECT * FROM auth_tokens WHERE selector = ?");
$stmt->execute([$tokenLeft]);
// Now our token data is stored in $row:
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// Delete token after being retrieved
$stmt = $this->db->prepare("DELETE FROM auth_tokens WHERE selector = ?");
$stmt->execute([$tokenLeft]);
// Verify the right hand side, hashed, matches the stored value:
if (!hash_equals($row['hash'], $tokenRightHashed)) {
throw new Exception('Invalid authentication token');
}
return $row['user_id'];
}
}
用法:
// Post-authentication, before headers are sent:
$cookieValue = $auth->createLongTermToken($userId);
setcookie(
'long_term_auth',
$cookieValue,
time() + (86400 * 30), // 30 days
'',
'',
TRUE, // Only send cookie over HTTPS, never unencrypted HTTP
TRUE // Don't expose the cookie to JavaScript
);
在页面加载时:
if (!isset($_SESSION['user_id']) && isset($_COOKIE['long_term_auth'])) {
try {
$_SESSION['user_id'] = $auth->loginWithPersistentCookie($_COOKIE['long_term_auth']);
} catch (Exception $ex) {
// Security error! Handle appropriately (i.e. log the incident).
}
}
上面的代码片段假定它$auth
是示例Authentication
类的一个实例。它还假设auth_tokens
(user_id
指向 users 表,selector
并且hash
是 VARCHAR 或 TEXT 字段,具有唯一约束selector
) 的基本表结构。
为什么这段代码是安全的?
- 它将长期身份验证 cookie 与用户密码分开。
- 令牌只允许使用一次。
- 它用于
random_bytes()
生成安全令牌。 - 它使用 SHA256(而不是 MD5)来存储较大的身份验证部分。令牌。
- 它用于
hash_equals()
比较哈希。 - 它仅通过 HTTPS 发送长期身份验证 cookie。
- 它使用 PHP 的内置会话管理功能。
为什么问题的代码不安全?
- 它使用 MD5,它不是一个安全的散列函数。
- 它使用
==
而不是hash_equals()
比较哈希。另请参阅:魔术哈希。
此外,您需要使用password_hash()
和password_verify()
进行实际的用户身份验证步骤。这只是描述了“记住我”复选框便利功能的安全实现。
为了更好的安全性,您可能需要使用password_exposed
和/或zxcvbn 之类的东西来防止使用弱/受损密码。
您的用户将希望使用密码管理器(例如 KeePassXC、1Password 或 LastPass)来为您的网站生成/存储他们的密码,这样他们就不会使用弱密码(例如admin
.
推荐阅读
- java - 如何使用 Java/sql/html 对大量数据进行分页?
- vbscript - 第一次尝试编码,但文件位置失败
- php - 使用 Docker 运行 cronjobs 时应该使用 docker exec 吗?
- java - 每个客户端都有子缓存的 Java 内存缓存
- unit-testing - 反应织物下拉单元测试
- html - 需要 Web 开发人员的帮助
- javascript - 使用 toJSON 进行字符串化时出现 Lodash deepClone 问题
- javascript - 收到的道具在反应本机导航中未定义
- unity3d - Unity 2018.3:如何在整个 3D 模型的一部分上应用纹理?
- python - 解码使用 CP437 表编码的文件