“让我保持登录状态” - 最好的方法

2022-08-30 06:05:31

我的 Web 应用程序使用会话在用户登录后存储有关用户的信息,并在用户在应用内从一个页面移动到另一个页面时维护该信息。在此特定应用程序中,我存储了该人的 、 和 。user_idfirst_namelast_name

我想在登录时提供“使我保持登录状态”选项,该选项将在用户的计算机上放置一个cookie两周,当他们返回应用程序时,它将使用相同的详细信息重新启动会话。

执行此操作的最佳方法是什么?我不想将它们存储在cookie中,因为这似乎会使一个用户很容易尝试伪造另一个用户的身份。user_id


答案 1

好吧,让我直截了当地说:如果你为此目的将用户数据或从用户数据派生的任何内容放入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);
        }
    }
}

注意:请勿使用令牌或用户和令牌的组合在数据库中查找记录。始终确保根据用户获取记录,并在之后使用计时安全比较函数来比较提取的令牌。有关定时攻击的更多信息

现在,成为加密秘密(由类似和/或从高熵输入派生的东西生成)非常重要。另外,需要是一个强随机源(几乎不够强。使用库,如 RandomLibrandom_compat,或与 )...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;
}

答案 2

安全声明:将 Cookie 基于确定性数据的 MD5 哈希值是一个坏主意;最好使用从CSPRNG派生的随机令牌。请参阅ircmaxell对此问题的回答,以获取更安全的方法。

通常我会做这样的事情:

  1. 用户使用“使我保持登录状态”登录
  2. 创建会话
  3. 创建一个名为 SOMETHING 的 Cookie,其中包含:md5(salt+username+ip+salt)和一个名为 somethingElse 的 cookie,其中包含 id
  4. 将 Cookie 存储在数据库中
  5. 用户做东西,离开----
  6. 用户返回,检查某些内容Else cookie,如果存在,请从该用户的数据库中获取旧哈希,检查cookie的内容与数据库中的哈希匹配,这也应与新计算的哈希值(对于ip)匹配,因此:cookieHash==databaseHash==md5(salt+username+ip+salt),如果他们这样做, 转到 2,如果他们不转到 1

当然,您可以使用不同的cookie名称等,您也可以稍微更改一下cookie的内容,只需确保它不容易创建即可。例如,您还可以在创建用户时创建user_salt,并将其放入cookie中。

您也可以使用sha1而不是md5(或几乎任何算法)


推荐