[php] “让我登录” - 最好的方法


Answers

好的,让我直言不讳地说:如果您将用户数据或从用户数据派生出来的任何数据放入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次的速度对该数字进行暴力破解。 事实上,你的服务器会在这样的负载下融化,但我们来玩吧。

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 sextillion秒...

让我们试着将其归结为更友好的数字。

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()不够强大)。使用一个库,例如RandomLibrandom_compatmcrypt_create_iv()DEV_URANDOM )。

hash_equals()是为了防止计时攻击 。 如果您使用低于PHP 5.6的PHP版本,则不支持函数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;
}
Question

我的Web应用程序使用会话在用户登录后存储有关用户的信息,并在应用程序内的页面之间传输信息时维护该信息。 在这个特定的应用程序中,我存储了该用户的user_idfirst_namelast_name

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

这样做的最佳方法是什么? 我不想将它们的user_id存储在cookie中,因为它似乎会使一个用户轻松尝试伪造另一个用户的身份。







旧线程,但仍然是一个有效的关注。 我注意到了一些关于安全性的良好回应,并避免使用'通过默默无闻的安全',但实际的技术方法在我眼中是不够的。 在我提供我的方法之前我必须说的事情:

  • 切勿以明文存储密码...永远!
  • 切勿将用户的散列密码存储在数据库中的多个位置。 您的服务器后端始终能够从用户表中提取散列的密码。 存储冗余数据代替额外的数据库事务并不是更高效,反过来说也是如此。
  • 您的会话ID应该是唯一的,所以没有两个用户可以共享ID,因此ID的用途(您的驾驶执照ID号码能否与其他人匹配?否)。这会生成两件式独特组合,基于2独特的弦乐。 你的会话表应该使用ID作为PK。 要允许多个设备被信任用于自动登录,请使用另一个包含所有验证设备列表(参见下面的示例)的受信任设备的表格,并使用用户名进行映射。
  • 它没有任何目的将已知数据散列到cookie中,可以复制cookie。 我们正在寻找的是一个遵从用户的设备,以提供真正的信息,这是在攻击者没有损害用户机器的情况下无法获得的(再次参见我的示例)。 然而,这意味着合法用户禁止他的机器的静态信息(即MAC地址,设备主机名,用户权限,如果受到浏览器的限制等)保持不变(或首先欺骗它)将无法使用此功能。 但是,如果这是一个问题,考虑一下这样的事实,即您提供自动登录的用户身份唯一标识 ,因此如果他们拒绝通过欺骗他们的MAC,欺骗他们的用户,欺骗/更改他们的主机名,躲在代理人后面,那么它们是不可识别的,并且永远不应该对自动服务进行认证。 如果你想要这样做,你需要考虑与客户端软件绑定的智能卡访问,这些软件为所使用的设备建立身份。

所有人都说,有两种很好的方法可以在你的系统上进行自动登录。

首先,便宜,简单的方法,把它全部放在别人身上。 如果您的网站支持使用google +帐户进行登录,那么您可能会有一个简化的Google +按钮,如果用户已登录到Google,则会将用户登录(我在此处回答此问题,因为我总是登录到谷歌)。 如果您希望用户自动登录(如果他们已使用受信任和受支持的身份验证程序登录)并选中此框,请在加载之前让您的客户端脚本在相应的“使用登录”按钮后面执行代码,只要确保让服务器在自动登录表中存储一个唯一的ID,该表包含用户名,会话ID和用户使用的身份验证器。 由于这些登录方法使用AJAX,因此您仍在等待响应,并且该响应要么是经过验证的响应,要么是拒绝响应。 如果您获得验证的响应,请按正常方式使用它,然后继续正常加载登录的用户。 否则,登录失败,但不告诉用户,只要继续未登录,他们会注意到。 这是为了防止攻击者窃取cookie(或者伪造他们以试图提升权限),从而获知用户自动登录到网站。

这很便宜,也可能被一些人认为是肮脏的,因为它试图验证你可能已经与Google和Facebook等地方签约,甚至没有告诉你。 但是,它不应该用于没有要求自动登录您的网站的用户,而且此特定方法仅适用于外部身份验证,例如使用Google+或FB。

由于使用外部身份验证程序在幕后告诉服务器是否验证了用户,因此攻击者无法获取除独有ID以外的任何内容,而这些ID本身无用。 我会详细说明:

  • 用户'joe'第一次访问网站,会话ID被放置在cookie'会话'中。
  • 用户'joe'登录,升级权限,获取新的会话ID并更新cookie'会话'。
  • 用户'joe'选择使用google +自动登录,获取放置在Cookie'keepmesignedin'中的唯一ID。
  • 用户'乔'谷歌让他们登录,让您的网站自动登录用户在您的后端使用谷歌。
  • 攻击者系统地为'keepmesignedin'尝试唯一的ID(这是向每个用户发布的公共知识),并且不会在其他任何地方登录; 尝试赋予'joe'的唯一ID。
  • 服务器收到“joe”的唯一ID,并在数据库中为一个Google +帐户提取匹配。
  • 服务器发送攻击者登录页面,运行一个AJAX请求到谷歌登录。
  • Google服务器收到请求,使用其API查看攻击者当前未登录。
  • Google会通过此连接发送当前没有登录用户的回复。
  • 攻击者的页面接收到响应,脚本自动重定向到登录页面,并使用在URL中编码的POST值。
  • 登录页面获取POST值,将'keepmesignedin'的cookie发送到一个空值并且有效期为1-1-1970,以阻止自动尝试,从而导致攻击者的浏览器简单地删除cookie。
  • 攻击者获得正常的首次登录页面。

无论如何,即使攻击者使用不存在的ID,除非收到验证的响应,否则尝试都将失败。

对于那些使用外部身份验证程序登录您的网站的用户,此方法可以并且应该与您的内部身份验证程序一起使用。

=========

现在,为了您自己的可以自动登录用户的身份验证器系统,这就是我的做法:

DB有几个表格:

TABLE users:
UID - auto increment, PK
username - varchar(255), unique, indexed, NOT NULL
password_hash - varchar(255), NOT NULL
...

请注意,用户名长度可达255个字符。 我的服务器程序在我的系统中将用户名限制为32个字符,但外部认证程序的用户名可能会比@ domain.tld大,所以我只支持最大长度的电子邮件地址以实现最大的兼容性。

TABLE sessions:
session_id - varchar(?), PK
session_token - varchar(?), NOT NULL
session_data - MediumText, NOT NULL

请注意,此表中没有用户字段,因为用户名在登录时位于会话数据中,且程序不允许空数据。 session_id和session_token可以使用随机md5散列,sha1 / 128/256散列,带有随机字符串的日期时间戳添加到它们然后散列,或者任何你想要的,但是你的输出的熵应该保持尽可能高缓解暴力攻击,甚至可以缓解压力,并且在尝试添加它们之前,应该检查会话类生成的所有哈希以查找会话表中的匹配项。

TABLE autologin:
UID - auto increment, PK
username - varchar(255), NOT NULL, allow duplicates
hostname - varchar(255), NOT NULL, allow duplicates
mac_address - char(23), NOT NULL, unique
token - varchar(?), NOT NULL, allow duplicates
expires - datetime code

MAC地址本质上应该是UNIQUE,因此每个条目都有一个唯一的值是有意义的。 另一方面,主机名可以合法地在不同的网络上复制。 有多少人使用“Home-PC”作为他们的计算机名称之一? 用户名由服务器后端从会话数据中提取,因此不可能进行操作。 至于令牌,应使用相同的方法为页面生成会话令牌,以便在用户自动登录的cookie中生成令牌。 最后,当用户需要重新验证他们的凭证时,添加日期时间代码。 可以在用户登录后的几天内更新此日期时间,或者无论上次登录是否只保留一个月左右,无论您的设计是什么,都可以强制其过期。

这可以防止某人系统地欺骗他们知道自动登录的用户的MAC和主机名。 绝不允许用户使用密码,明文或其他方式保存cookie。 让每个页面导航都重新生成令牌,就像会话令牌一样。 这大大降低了攻击者获得有效令牌cookie并使用它登录的可能性。 有些人会试图说,攻击者可以从受害者那里窃取cookie,并进行会话重播攻击来登录。 如果攻击者可以窃取cookie(这是可能的),那么他们肯定会损害整个设备,这意味着他们可以仅仅使用该设备登录,这完全破坏了窃取cookie的目的。 只要您的站点通过HTTPS(在处理密码,CC号码或其他登录系统时运行),您就可以在浏览器中为用户提供所有的保护。

有一件事要记住:如果您使用自动登录,会话数据不应该过期。 您可以过期错误地继续会话,但在系统中进行验证应恢复会话数据(如果数据是预期在会话之间继续存在的持久数据)。 如果您想要持久性和非持久性会话数据,请使用另一个表作为持久性会话数据,使用用户名作为PK,并让服务器像检查普通会话数据一样检索它,只需使用另一个变量即可。

一旦以这种方式实现登录,服务器应该仍然验证会话。 这是您可以编码被盗或受损系统的期望值的地方; 模式和登录会话数据的其他预期结果通常会导致系统被劫持或cookie被伪造以获取访问权的结论。 这是ISS Tech可以将触发帐户锁定或自动从用户自动登录系统中移除的规则,让攻击者足够长时间让用户确定攻击者成功的方式以及如何将其切断。

作为结束语,请确保任何恢复尝试,密码更改或超过阈值的登录失败都会导致自动登录被禁用,直到用户正确验证并确认已发生。

我很抱歉,如果有人希望在我的回答中发布代码,这不会发生在这里。 我会说我使用PHP,jQuery和AJAX来运行我的网站,并且我永远不会使用Windows作为服务器......。




I don't understand the concept of storing encrypted stuff in a cookie when it is the encrypted version of it that you need to do your hacking. If I'm missing something, please comment.

I am thinking about taking this approach to 'Remember Me'. If you can see any issues, please comment.

  1. Create a table to store "Remember Me" data in - separate to the user table so that I can log in from multiple devices.

  2. 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

  3. 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.

  4. On logout, Mark all the "Remember Me" records for that user as expired.

The only vulnerabilities that I can see is;

  • you could get hold of someone's laptop and spoof their IP address with the cookie.
  • you could spoof a different IP address each time and guess the whole thing - but with two big string to match, that would be...doing a similar calculation to above...I have no idea...huge odds?



Implementing a "Keep Me Logged In" feature means you need to define exactly what that will mean to the user. In the simplest case, I would use that to mean the session has a much longer timeout: 2 days (say) instead of 2 hours. To do that, you will need your own session storage, probably in a database, so you can set custom expiry times for the session data. Then you need to make sure you set a cookie that will stick around for a few days (or longer), rather than expire when they close the browser.

I can hear you asking "why 2 days? why not 2 weeks?". This is because using a session in PHP will automatically push the expiry back. This is because a session's expiry in PHP is actually an idle timeout.

Now, having said that, I'd probably implement a harder timeout value that I store in the session itself, and out at 2 weeks or so, and add code to see that and to forcibly invalidate the session. Or at least to log them out. This will mean that the user will be asked to login periodically. 雅虎 does this.







Links