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

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

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


当前回答

我推荐Stefan提到的方法(即遵循改进的持久登录Cookie最佳实践中的指导方针),也建议你确保你的Cookie是HttpOnly Cookie,这样它们就不会被潜在的恶意JavaScript访问。

其他回答

我的解是这样的。它不是百分之百的防弹,但我认为它在大多数情况下都能帮你。

当用户成功登录时,创建一个包含以下信息的字符串:

$data = (SALT + ":" + hash(User Agent) + ":" + username 
                     + ":" + LoginTimestamp + ":"+ SALT)

加密$data,设置类型为HttpOnly,并设置cookie。

当用户回到你的网站时,执行以下步骤:

Get cookie data. Remove dangerous characters inside cookie. Explode it with : character. Check validity. If cookie is older than X days then redirect user to login page. If cookie is not old; Get latest password change time from database. If password is changed after user's last login redirect user to login page. If pass wasn't changed recently; Get user's current browser agent. Check whether (currentUserAgentHash == cookieUserAgentHash). IF agents are same go to next step, else redirect to login page. If all steps passed successfully authorize username.

如果用户登出,请删除此cookie。如果用户重新登录,创建新的cookie。

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

安全注意:基于确定性数据的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链接。

基本上,您不会将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;
}