好吧,让我直截了当地说:如果您为此目的将用户数据或从用户数据派生的任何内容放入 cookie,那么您做错了什么。
那里。我说了。现在我们可以继续讨论实际的答案。
你问,散列用户数据有什么问题?嗯,它归结为暴露表面和通过默默无闻的安全性。
想象一下,你是一个攻击者。您会在会话中看到为 remember-me 设置的加密 cookie。它有 32 个字符宽。哎呀。那可能是MD5...
让我们再想象一下,他们知道您使用的算法。例如:
md5(salt+username+ip+salt)
现在,攻击者需要做的就是暴力破解“盐”(这实际上不是盐,但稍后会详细介绍),他现在可以使用他的 IP 地址的任何用户名生成他想要的所有假令牌!但是暴力破解盐很难,对吧?绝对地。但现代 GPU 非常擅长它。除非您在其中使用足够的随机性(使其足够大),否则它会迅速下降,并随之成为您城堡的钥匙。
简而言之,唯一能保护你的是盐,它并没有你想象的那样真正保护你。
可是等等!
所有这些都假设攻击者知道算法!如果它是秘密和令人困惑的,那么你是安全的,对吧?错了。这种思路有一个名字:Security Through Obscurity,永远不应该依赖它。
更好的方式
更好的方法是永远不要让用户的信息离开服务器,除了 id。
当用户登录时,生成一个大的(128 到 256 位)随机令牌。将其添加到将令牌映射到用户 ID 的数据库表中,然后将其发送到 cookie 中的客户端。
如果攻击者猜到另一个用户的随机令牌怎么办?
好吧,让我们在这里做一些数学运算。我们正在生成一个 128 位随机令牌。这意味着有:
possibilities = 2^128
possibilities = 3.4 * 10^38
现在,为了说明这个数字有多大,让我们想象一下互联网上的每台服务器(假设今天有 50,000,000 台)试图以每台每秒 1,000,000,000 的速度暴力破解该数字。实际上,您的服务器会在这样的负载下融化,但让我们发挥一下。
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
所以每秒有 50 万亿次猜测。真快!对?
time_to_guess = possibilities / guesses_per_second
time_to_guess = 3.4e38 / 50,000,000,000,000,000
time_to_guess = 6,800,000,000,000,000,000,000
所以 6.8 六亿秒...
让我们尝试将其归结为更友好的数字。
215,626,585,489,599 years
甚至更好:
47917 times the age of the universe
是的,那是宇宙年龄的 47917 倍……
基本上,它不会被破解。
所以总结一下:
我推荐的更好的方法是将 cookie 存储为三个部分。
function onLogin($user) {
$token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
storeTokenForUser($user, $token);
$cookie = $user . ':' . $token;
$mac = hash_hmac('sha256', $cookie, SECRET_KEY);
$cookie .= ':' . $mac;
setcookie('rememberme', $cookie);
}
然后,验证:
function rememberMe() {
$cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
if ($cookie) {
list ($user, $token, $mac) = explode(':', $cookie);
if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
return false;
}
$usertoken = fetchTokenByUserName($user);
if (hash_equals($usertoken, $token)) {
logUserIn($user);
}
}
}
注意:不要使用令牌或用户和令牌的组合在数据库中查找记录。始终确保根据用户获取记录,然后使用时间安全比较功能来比较获取的令牌。更多关于定时攻击的信息。
现在,它是一个密码秘密(由类似的东西生成和/或从高熵输入派生)是非常重要的。此外,需要是一个强大的随机源(还不够强大。使用库,例如 RandomLib或random_compat,或with )...SECRET_KEY/dev/urandomGenerateRandomToken()mt_rand()mcrypt_create_iv()DEV_URANDOM
hash_equals()就是防止定时攻击。如果您使用低于 PHP 5.6 的 PHP 版本,hash_equals()则不支持该功能。在这种情况下,您可以替换hash_equals()为 timingSafeCompare 函数:
/**
* A timing safe equals comparison
*
* To prevent leaking length information, it is important
* that user input is always used as the second parameter.
*
* @param string $safe The internal (safe) value to be checked
* @param string $user The user submitted (unsafe) value
*
* @return boolean True if the two strings are identical.
*/
function timingSafeCompare($safe, $user) {
if (function_exists('hash_equals')) {
return hash_equals($safe, $user); // PHP 5.6
}
// Prevent issues if string length is 0
$safe .= chr(0);
$user .= chr(0);
// mbstring.func_overload can make strlen() return invalid numbers
// when operating on raw binary strings; force an 8bit charset here:
if (function_exists('mb_strlen')) {
$safeLen = mb_strlen($safe, '8bit');
$userLen = mb_strlen($user, '8bit');
} else {
$safeLen = strlen($safe);
$userLen = strlen($user);
}
// Set the result to the difference between the lengths
$result = $safeLen - $userLen;
// Note that we ALWAYS iterate over the user-supplied length
// This is to prevent leaking length information
for ($i = 0; $i < $userLen; $i++) {
// Using % here is a trick to prevent notices
// It's safe, since if the lengths are different
// $result is already non-0
$result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}