好吧,让我直截了当地说:如果你为此目的将用户数据或从用户数据派生的任何内容放入cookie中,那么你做错了什么。
那里。我说了。现在我们可以继续讨论实际的答案。
你问哈希用户数据有什么问题?好吧,它归结为曝光表面和通过晦涩的安全性。
想象一下,你是一个攻击者。您会在会话中看到为“记住我”设置的加密 Cookie。它有 32 个字符宽。哎呀。这可能是一个MD5...
让我们也想象一下,他们知道你使用的算法。例如:
md5(salt+username+ip+salt)
现在,攻击者需要做的就是暴力破解“盐”(这不是真正的盐,但稍后会详细介绍),他现在可以用他的IP地址的任何用户名生成他想要的所有假令牌!但是暴力破解盐很难,对吧?绝对。但现代GPU非常擅长于此。除非你在其中使用足够的随机性(使它足够大),否则它会很快掉落,并随之成为你城堡的钥匙。
简而言之,唯一能保护你的就是盐,它并不像你想象的那么真正保护你。
但是等等!
所有这些都预示着攻击者知道算法!如果它是秘密和令人困惑的,那么你是安全的,对吧?错了。这种思路有一个名字:“通过默默无闻实现安全”,永远不应该依赖它。
更好的方式
更好的方法是永远不要让用户的信息离开服务器,除了id。
当用户登录时,生成一个大型(128 到 256 位)随机令牌。将其添加到将令牌映射到用户标识的数据库表中,然后将其发送到 Cookie 中的客户端。
如果攻击者猜测另一个用户的随机令牌怎么办?
好吧,让我们在这里做一些数学运算。我们正在生成一个 128 位随机令牌。这意味着有:
possibilities = 2^128
possibilities = 3.4 * 10^38
现在,为了显示这个数字是多么荒谬,让我们想象一下互联网上的每个服务器(假设今天有50,000,000台)都试图以每秒1,000,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,或与 )...SECRET_KEY
/dev/urandom
GenerateRandomToken()
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;
}