我的web应用程序使用会话存储关于用户的信息,一旦他们登录,并维护这些信息,因为他们在应用程序内从页面到页面。在这个特定的应用程序中,我存储的人的user_id, first_name和last_name。

我想在登录时提供一个“让我登录”选项,在用户的机器上放置一个cookie,为期两周,当他们返回应用程序时,将以相同的细节重新启动他们的会话。

做这件事的最佳方法是什么?我不想在cookie中存储他们的user_id,因为这似乎会让一个用户很容易尝试和伪造另一个用户的身份。


当前回答

安全注意:基于确定性数据的MD5哈希的cookie是一个坏主意;最好使用从CSPRNG派生的随机令牌。有关更安全的方法,请参阅ircmaxell对这个问题的回答。

通常我会这样做:

User logs in with 'keep me logged in' Create session Create a cookie called SOMETHING containing: md5(salt+username+ip+salt) and a cookie called somethingElse containing id Store cookie in database User does stuff and leaves ---- User returns, check for somethingElse cookie, if it exists, get the old hash from the database for that user, check of the contents of cookie SOMETHING match with the hash from the database, which should also match with a newly calculated hash (for the ip) thus: cookieHash==databaseHash==md5(salt+username+ip+salt), if they do, goto 2, if they don't goto 1

当然,你可以使用不同的cookie名称等,你也可以改变cookie的内容,只是要确保它不容易创建。例如,你也可以在创建用户时创建user_salt,并将其放在cookie中。

你也可以用sha1代替md5(或者几乎任何算法)

其他回答

安全注意:基于确定性数据的MD5哈希的cookie是一个坏主意;最好使用从CSPRNG派生的随机令牌。有关更安全的方法,请参阅ircmaxell对这个问题的回答。

通常我会这样做:

User logs in with 'keep me logged in' Create session Create a cookie called SOMETHING containing: md5(salt+username+ip+salt) and a cookie called somethingElse containing id Store cookie in database User does stuff and leaves ---- User returns, check for somethingElse cookie, if it exists, get the old hash from the database for that user, check of the contents of cookie SOMETHING match with the hash from the database, which should also match with a newly calculated hash (for the ip) thus: cookieHash==databaseHash==md5(salt+username+ip+salt), if they do, goto 2, if they don't goto 1

当然,你可以使用不同的cookie名称等,你也可以改变cookie的内容,只是要确保它不容易创建。例如,你也可以在创建用户时创建user_salt,并将其放在cookie中。

你也可以用sha1代替md5(或者几乎任何算法)

我不理解在cookie中存储加密内容的概念,因为你需要进行黑客操作的正是它的加密版本。如果我遗漏了什么,请评论。

我正在考虑采用这种方法来“记住我”。如果你能看到任何问题,请评论。

Create a table to store "Remember Me" data in - separate to the user table so that I can log in from multiple devices. On successful login (with Remember Me ticked): a) Generate a unique random string to be used as the UserID on this machine: bigUserID b) Generate a unique random string: bigKey c) Store a cookie: bigUserID:bigKey d) In the "Remember Me" table, add a record with: UserID, IP Address, bigUserID, bigKey If trying to access something that requires login: a) Check for the cookie and search for bigUserID & bigKey with a matching IP address b) If you find it, Log the person in but set a flag in the user table "soft login" so that for any dangerous operations, you can prompt for a full login. On logout, Mark all the "Remember Me" records for that user as expired.

我能看到的唯一弱点是;

你就可以拿到别人的笔记本电脑,用cookie欺骗他们的IP地址。 你可以每次欺骗一个不同的IP地址,并猜出整个事情-但有两个大字符串来匹配,这将是…做类似于上面的计算…我不知道……巨大的机会吗?

生成一个散列,其中可能包含只有您知道的秘密,然后将其存储在您的DB中,以便与用户关联。应该工作得很好。

我在这里问了这个问题的一个角度,答案将引导您找到所需的所有基于令牌的超时cookie链接。

基本上,您不会将userId存储在cookie中。您存储了一个一次性令牌(巨大的字符串),用户使用它来拾取旧的登录会话。然后,为了使其真正安全,在进行重大操作(比如更改密码本身)时,需要输入密码。

好吧,让我直截了当地说:如果你为了这个目的把用户数据,或者任何从用户数据衍生出来的东西放到cookie里,你就做错了。

在那里。我说了。现在我们来看看真正的答案。

你可能会问,哈希用户数据有什么问题?好吧,这归结于曝光表面和通过隐藏来保证安全。

想象一下,你是一个攻击者。您将看到为会话上的remember-me设置的加密cookie。它有32个字符宽。哇。那可能是MD5…

让我们再想象一下他们知道你用的算法。例如:

md5(salt+username+ip+salt)

现在,攻击者所需要做的就是强力破解“盐”(这并不是真正的盐,但稍后会详细介绍),他现在可以用他的IP地址的任何用户名生成他想要的所有假令牌!但是野蛮胁迫盐很难,对吧?绝对的。但现代gpu在这方面做得非常好。除非你在游戏中使用足够的随机性(游戏邦注:让游戏规模足够大),否则游戏便会迅速倒塌,并带走通往你城堡的钥匙。

简而言之,唯一能保护你的是盐,它并没有像你想象的那样保护你。

但是等等!

所有这些都是假设攻击者知道算法!如果是秘密和困惑,那你就安全了,对吧?错了。这种思路有一个名字:通过模糊获得安全,永远不应该依赖。

更好的方法

更好的方法是永远不要让用户的信息离开服务器,除了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

所以每秒要猜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的10的10次方秒…

让我们试着把它降低到更友好的数字。

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);
        }
    }
}

注意:不要使用令牌或用户和令牌的组合来查找数据库中的记录。始终确保根据用户获取记录,然后使用时间安全的比较函数来比较所获取的令牌。更多关于定时攻击的内容。

现在,非常重要的是,SECRET_KEY是一个加密秘密(由/dev/urandom之类的东西生成和/或从高熵输入派生)。此外,GenerateRandomToken()需要是一个强随机源(mt_rand()还不够强。使用一个库,如RandomLib或random_compat,或mcrypt_create_iv()与DEV_URANDOM)…

hash_equals()是为了防止定时攻击。 如果使用PHP 5.6以下的版本,则不支持hash_equals()函数。在这种情况下,你可以用timingSafeCompare函数替换hash_equals():

/**
 * 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;
}