我的Web应用程序使用会话来存储有关用户登录后的信息,并在应用程序中从页面到页面时维护这些信息。在此特定应用中,我正在存储user_id
,,,,first_name
和last_name
那个人。
我想在登录中提供"保持我登录"选项,该选项将在用户的计算机上放置两个星期,当他们返回应用程序时,它将以相同的详细信息重新启动其会话。
这样做的最佳方法是什么?我不想存储他们的user_id
在cookie中,这似乎使一个用户可以轻松尝试伪造另一个用户的身份。
答案
好的,让我直言不讳:如果您将用户数据或从用户数据派生的任何东西都放入cookie中,则为此目的,您会做错了什么。
那里。我说了。现在,我们可以继续进行实际答案。
您问哈希用户数据有什么问题?好吧,这归结于通过默默无闻的曝光表面和安全性。
想象一下您是攻击者。
我们还可以想象他们知道您使用的算法。例如:
md5(salt+username+ip+salt)
现在,攻击者所需要做的就是暴力破解"盐"(这并不是真正的盐,稍后会详细介绍),他现在可以使用其 IP 地址的任何用户名生成他想要的所有假令牌!
简而言之,唯一保护您的是盐,这并没有真正保护您的想象。
But Wait!
所有这些都表明攻击者知道算法!如果这是秘密且令人困惑,那么您是安全的,对吗?WRONG 。那条思维有一个名字:通过默默无闻的安全 ,应该NEVER被依靠。
The Better Way
更好的方法是不要让用户的信息离开服务器,除了ID。
当用户登录时,生成一个大的(128至256位)随机令牌。将其添加到数据库表中,该数据库表将令牌映射到UserID,然后将其发送给Cookie中的客户端。
如果攻击者猜测另一个用户的随机令牌怎么办?
好吧,让我们在这里做一些数学。我们正在生成128位随机令牌。这意味着有:
possibilities = 2^128
possibilities = 3.4 * 10^38
现在,要显示这个数字的荒谬,让我们想象一下Internet上的每台服务器(今天可以说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
所以5亿亿每秒的猜测。
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);
}
}
}
注意:请勿使用令牌或用户和令牌的组合来查找数据库中的记录。有关定时攻击的更多信息。
现在,是very 重要的是SECRET_KEY
是一个加密秘密(由类似的东西生成/dev/urandom
和/或源自高熵输入)。GenerateRandomToken()
需要是一个强随机源(mt_rand()
还不够强大。随机库或者随机兼容, 或者mcrypt_create_iv()
和DEV_URANDOM
)…
这hash_equals()
是为了防止定时攻击。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;
}