如何在 PHP 中限制用户登录尝试

Posted

技术标签:

【中文标题】如何在 PHP 中限制用户登录尝试【英文标题】:How can I throttle user login attempts in PHP 【发布时间】:2011-01-06 15:34:09 【问题描述】:

我刚刚阅读了这篇关于防止快速登录尝试的帖子The definitive guide to form-based website authentication。

最佳实践 #1:随着尝试失败次数的增加而增加的短时间延迟,例如:

1 次尝试失败 = 没有延迟 2 次失败尝试 = 2 秒延迟 3 次失败尝试 = 4 秒延迟 4 次失败尝试 = 8 秒延迟 5 次失败尝试 = 16 秒延迟 等等。

DoS 攻击这种方案是非常不切实际的,但另一方面,由于延迟呈指数增长,因此具有潜在的破坏性。

我很好奇如何在 PHP 中为我的登录系统实现这样的功能?

【问题讨论】:

只要确保没有算术溢出,否则您可能会得到负延迟。 不要延迟,在尝试 x 次后完全阻止。当机器人尝试登录时发送 404。没有理由通过调整延迟来过度复杂化。另外,人类会失败 3 次(并得到长时间的延迟)并不是不可能的 @sestocker 实际上,我在这里推荐418 I'm a teapot 而不是 404。 en.wikipedia.org/wiki/Http_status_codes ;o) @sestocker:出于多种原因,您不想阻止登录; DoS 是一个原因,实用性是另一个原因(如果您的网站有数百万个帐户,您不想手动重新激活您的用户)。此外,4 或 8 秒对于人类来说并不是一个“长”延迟,但在暴力攻击中却是一个严重的麻烦。不过,您可能应该将延迟限制在 15 或 30 分钟左右,并提供基于 IP 或 cookie 的“直接线路”,这样真实用户就不会因重复失败的尝试而被锁定。阅读链接的帖子了解详细信息。 4 或 8 秒 对于忘记密码并想要检查十几个变体的人来说是一个很长的延迟(“最后是 ! 还是 @ ?”)。看看 android 对屏幕锁定做了什么:5 次尝试,延迟,5 次尝试,延迟……这是一种更人性化的方法。 【参考方案1】:

您不能简单地通过将限制链接到单个 IP 或用户名来防止 DoS 攻击。使用这种方法,您甚至无法真正阻止快速登录尝试。

为什么? 因为为了绕过您的限制尝试,攻击可以跨越多个 IP 和用户帐户。

我在其他地方看到过帖子,理想情况下,您应该跟踪整个站点的所有失败登录尝试,并将它们与时间戳相关联,也许:

CREATE TABLE failed_logins (
    id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(16) NOT NULL,
    ip_address INT(11) UNSIGNED NOT NULL,
    attempted DATETIME NOT NULL,
    INDEX `attempted_idx` (`attempted`)
) engine=InnoDB charset=UTF8;

关于 ip_address 字段的简要说明:您可以分别使用 INET_ATON() 和 INET_NTOA() 存储数据和检索数据,这基本上等同于将 ip 地址转换为无符号整数。

# example of insertion
INSERT INTO failed_logins SET username = 'example', ip_address = INET_ATON('192.168.0.1'), attempted = CURRENT_TIMESTAMP;
# example of selection
SELECT id, username, INET_NTOA(ip_address) AS ip_address, attempted;

根据在给定时间(本例中为 15 分钟)内的总体失败登录次数确定某些延迟阈值。您应该基于从您的 failed_logins 表中提取的统计数据,因为它会随着时间的推移而变化,具体取决于用户的数量以及其中有多少人可以回忆(和输入)他们的密码。


> 10 failed attempts = 1 second
> 20 failed attempts = 2 seconds
> 30 failed attempts = reCaptcha

查询每次失败登录尝试的表,以查找给定时间段内失败的登录次数,例如 15 分钟:


SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute);

如果在给定时间段内的尝试次数超过您的限制,则强制限制或强制所有用户使用验证码(即 reCaptcha),直到给定时间段内的失败尝试次数小于阈值.

// array of throttling
$throttle = array(10 => 1, 20 => 2, 30 => 'recaptcha');

// retrieve the latest failed login attempts
$sql = 'SELECT MAX(attempted) AS attempted FROM failed_logins';
$result = mysql_query($sql);
if (mysql_affected_rows($result) > 0) 
    $row = mysql_fetch_assoc($result);

    $latest_attempt = (int) date('U', strtotime($row['attempted']));

    // get the number of failed attempts
    $sql = 'SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute)';
    $result = mysql_query($sql);
    if (mysql_affected_rows($result) > 0) 
        // get the returned row
        $row = mysql_fetch_assoc($result);
        $failed_attempts = (int) $row['failed'];

        // assume the number of failed attempts was stored in $failed_attempts
        krsort($throttle);
        foreach ($throttle as $attempts => $delay) 
            if ($failed_attempts > $attempts) 
                // we need to throttle based on delay
                if (is_numeric($delay)) 
                    $remaining_delay = time() - $latest_attempt - $delay;
                    // output remaining delay
                    echo 'You must wait ' . $remaining_delay . ' seconds before your next login attempt';
                 else 
                    // code to display recaptcha on login form goes here
                
                break;
            
                
    

在特定阈值下使用 reCaptcha 将确保阻止来自多个方面的攻击,并且正常站点用户不会因为合法的失败登录尝试而遇到明显延迟。

【讨论】:

这是一个很好的观点,我实际上是在考虑这个,因为我见过可以尝试在 myspace 上使用 100,000 次登录的电子邮件/密码文件和另一个要使用的 IP 地址密码登录的软件,然后它可以以某种方式更改每个请求的 IP,这样就可以阻止我认为的事情 需要注意的是,限制时间应该足够低,不会惹恼普通用户,但要足够长,以阻止机器人重复触发 cURL 请求。用户甚至不会注意到 2 秒的延迟,因为他们的下一次登录尝试可能会比上一次尝试超过 2 秒。另一方面,机器人会因为必须等待 2 秒才能再次尝试而受到很大影响。脚本小子很可能会去其他地方,因为小的延迟会大大减少他们可以提出的总体请求数量。 我喜欢这个主意。也许你可以看看这篇文章:***.com/questions/479233/… 它讨论了完全相同的问题(分布式蛮力),如果你也可以在那里详细发布你的想法会很好 我认为“$remaining_delay = time() - $latest_attempt - $delay;”是错的。不应该是这样吗?:“$remaining_delay = $delay - (time() - $latest_attempt);” 我根据您在此评论中阐述的概念创建了一个类,并针对任何想要使用它的人进行了一些改进,称为 BruteForceBlocker github.com/ejfrancis/BruteForceBlocker【参考方案2】:

您有三种基本方法:存储会话信息、存储 cookie 信息或存储 IP 信息。

如果您使用会话信息,最终用户(攻击者)可以强行调用新会话,绕过您的策略,然后立即重新登录。会话实现起来非常简单,只需将用户的最后一次已知登录时间存储在会话变量中,将其与当前时间进行匹配,并确保延迟足够长。

如果你使用 cookie,攻击者可以简单地拒绝 cookie,总而言之,这确实是不可行的。

如果您跟踪 IP 地址,您需要以某种方式存储来自 IP 地址的登录尝试,最好是在数据库中。当用户尝试登录时,只需更新您记录的 IP 列表。您应该以合理的时间间隔清除此表,转储一段时间内未激活的 IP 地址。陷阱(总是有陷阱)是一些用户最终可能会共享一个 IP 地址,并且在边界条件下,您的延迟可能会无意中影响用户。由于您正在跟踪失败的登录,并且只跟踪失败的登录,因此这不会造成太大的痛苦。

【讨论】:

IP 地址不是一个好的解决方案:1)它们经常被共享 2)使用 TOR 很容易不断更改地址 @symcbean 我已经解决了多种解决方案,它们的任何组合都会阻止一些攻击者,没有神奇的解决方案。正如我在回答中所讨论的那样,共享 IP 地址不是问题;有人可能会使用 TOR 更改它似乎比强迫新会话的人更不可能。我错过了第四个选项吗? @symcbean 如果使用 TOR,通常会通过涉及的多个层的开销进行隐式节流。根据其设计,TOR 必然会使暴力攻击变得复杂。【参考方案3】:

简短的回答是:不要这样做。您不会保护自己免受暴力破解,甚至可能使您的情况变得更糟。

建议的解决方案都不起作用。如果您使用 IP 作为任何限制参数,攻击者将只会跨越大量 IP 进行攻击。如果您使用会话(cookie),攻击者只会丢弃任何 cookie。你能想到的总和是,绝对没有什么是暴力破解者无法克服的。

但有一件事 - 您只依赖尝试登录的用户名。因此,不要查看所有其他参数,您可以跟踪用户尝试登录和限制的频率。但是攻击者想要伤害你。如果他认识到这一点,他也会暴力破解用户名。

这将导致您的几乎所有用户在尝试登录时都被限制到您的最大值。您的网站将毫无用处。攻击者:成功。

您通常可以将密码检查延迟大约 200 毫秒 - 网站用户几乎不会注意到这一点。但是一个蛮力的人会。 (同样,他可以跨越 IP)但是,所有这些都不能保护您免受暴力破解或 DDoS 攻击 - 因为您不能以编程方式。

做到这一点的唯一方法是使用基础设施。

您应该使用 bcrypt 而不是 MD5 或 SHA-x 来散列您的密码,如果有人窃取您的数据库(因为我猜您在共享或托管主机上),这将使您的密码解密变得更加困难

很抱歉让您失望了,但是这里所有的解决方案都有一个弱点,并且在后端逻辑中没有办法克服它们。

【讨论】:

【参考方案4】:

登录过程需要降低成功和不成功登录的速度。登录尝试本身的速度永远不应超过 1 秒。如果是,蛮力使用延迟来知道尝试失败,因为成功比失败短。然后,每秒可以评估更多组合。

每台机器的同时登录尝试次数需要受到负载平衡器的限制。最后,您只需要跟踪相同的用户或密码是否被多次用户/密码登录尝试重复使用。人类的打字速度不能超过每分钟 200 个字。因此,每分钟超过 200 个字的连续或同时登录尝试来自一组机器。因此,这些可以安全地通过管道传输到黑名单,因为它不是您的客户。每台主机的黑名单时间不需要大于约 1 秒。这绝不会给人类带来不便,但无论是串行还是并行,都会对蛮力尝试造成严重破坏。

2 * 10^19 个组合,每秒一个组合,在 40 亿个独立 IP 地址上并行运行,作为搜索空间需要 158 年才能耗尽。要让每个用户持续一天对抗 40 亿攻击者,您需要一个至少 9 位长度的完全随机的字母数字密码。考虑用至少 13 位长、1.7 * 10^20 组合的密码短语训练用户。

这种延迟会促使攻击者窃取您的密码哈希文件,而不是暴力破解您的网站。使用经批准的、命名的散列技术。禁止整个互联网 IP 人口一秒钟,将限制并行攻击的影响,而无需人类愿意接受的交易。最后,如果您的系统在一秒钟内允许超过 1000 次失败的登录尝试而对禁止系统没有任何响应,那么您的安全计划就有更大的问题需要解决。首先修复该自动响应。

【讨论】:

【参考方案5】:
session_start();
$_SESSION['hit'] += 1; // Only Increase on Failed Attempts
$delays = array(1=>0, 2=>2, 3=>4, 4=>8, 5=>16); // Array of # of Attempts => Secs

sleep($delays[$_SESSION['hit']]); // Sleep for that Duration.

或者按照 Cyro 的建议:

sleep(2 ^ (intval($_SESSION['hit']) - 1));

这有点粗糙,但基本组件都在那里。如果刷新此页面,每次刷新延迟都会变长。

您还可以将计数保存在数据库中,您可以在其中按 IP 检查失败尝试的次数。通过基于 IP 使用它并将数据保存在您身边,您可以防止用户清除他们的 cookie 以阻止延迟。

基本上,开始的代码是:

$count = get_attempts(); // Get the Number of Attempts

sleep(2 ^ (intval($count) - 1));

function get_attempts()

    $result = mysql_query("SELECT FROM TABLE WHERE IP=\"".$_SERVER['REMOTE_ADDR']."\"");
    if(mysql_num_rows($result) > 0)
    
        $array = mysql_fetch_assoc($array);
        return $array['Hits'];
    
    else
    
        return 0;
    

【讨论】:

也可以使用:sleep(2 ^ (intval($_SESSION['hit']) - 1)); 一个明显的问题是严重的暴力攻击者不会费心实际处理 cookie,因此会话变得毫无价值。 睡眠(2 ^ (intval($count) - 1));我有点喜欢这个数组,所以我可以设置等待的时间,但我很好奇,这如何等同?另外,如果我要保存到数据库,一旦用户登录,我是否会从数据库中删除命中,这样当他们下次登录后尝试登录时,这是一个全新的开始? 您可以设置过期时间,因为延迟应该在一定时间后过期。其他任何事情都取决于你。如果有人登录/注销并尝试重新登录,您可能希望也可能不希望保留他们过去的延迟计时器。那是你的电话。 还要记住,Cryo 的答案不使用数组。【参考方案6】:

通过 IP 在数据库中存储失败尝试。 (因为你有一个登录系统,我假设你很清楚如何做到这一点。)

显然,会话是一种诱人的方法,但真正敬业的人可以很容易地意识到,他们可以在尝试失败时简单地删除会话 cookie,以完全规避限制。

在尝试登录时,获取最近(例如,过去 15 分钟)的登录尝试次数,以及最近一次尝试的时间。

$failed_attempts = 3; // for example
$latest_attempt = 1263874972; // again, for example
$delay_in_seconds = pow(2, $failed_attempts); // that's 2 to the $failed_attempts power
$remaining_delay = time() - $latest_attempt - $delay_in_seconds;
if($remaining_delay > 0) 
    echo "Wait $remaining_delay more seconds, silly!";

【讨论】:

数据库绝对是做到这一点的方法。这样一来,您还可以回顾历史。 我在想这样的事情,我认为 vbulletin 论坛会这样做,我认为可以通过关闭浏览器并返回来重置会话 你能解释一下这会产生什么样的时间 pow(2, $failed_attempts) ? 我不建议你使用睡眠,因为它会阻塞 php 的实例直到睡眠结束。如果攻击者打开一堆连接来暴力破解服务器,它会很快用 PHP 请求进行备份。最好在该 IP 的“延迟”时间段内使所有登录尝试失败。 我会限制$remaining_delay = min(3600, $remaining_delay);【参考方案7】:

您可以使用会话。每当用户登录失败时,您都会增加存储尝试次数的值。您可以从尝试次数中计算出所需的延迟,也可以设置允许用户在会话中再次尝试的实际时间。

更可靠的方法是将尝试和新尝试时间存储在该特定 IP 地址的数据库中。

【讨论】:

我目前正在做这样的事情,但我在想是否存在 DoS 攻击> 机器人可以轻松选择忽略会话 cookie。使用带 IP 的数据库,机器人除了切换 IP 之外什么也做不了。 @Matchu - 如果您这样做,您可能会进行数千或数百万次不必要的数据库调用,并以其他方式占用您的资源。我相信有比您的建议更好的组合解决方案。【参考方案8】:

恕我直言,防御 DOS 攻击最好在 Web 服务器级别(甚至可能在网络硬件中)处理,而不是在您的 PHP 代码中。

【讨论】:

没错,但有时你必须用手中的棍子战斗。【参考方案9】:

我通常会创建登录历史记录和登录尝试表。尝试表将记录用户名、密码、IP 地址等。查询表以查看是否需要延迟。我建议在给定时间(例如一小时)内完全阻止超过 20 次的尝试。

【讨论】:

【参考方案10】:

根据上面的讨论,会话、cookie 和 IP 地址是无效的 - 都可以被攻击者操纵。

如果您想防止暴力攻击,那么唯一可行的解​​决方案是将尝试次数基于提供的用户名,但请注意,这允许攻击者通过阻止有效用户登录来对站点进行 DOS。

例如

$valid=check_auth($_POST['USERNAME'],$_POST['PASSWD']);
$delay=get_delay($_POST['USERNAME'],$valid);

if (!$valid) 
   header("Location: login.php");
   exit;

...
function get_delay($username,$authenticated)

    $loginfile=SOME_BASE_DIR . md5($username);
    if (@filemtime($loginfile)<time()-8600) 
       // last login was never or over a day ago
       return 0;
    
    $attempts=(integer)file_get_contents($loginfile);
    $delay=$attempts ? pow(2,$attempts) : 0;
    $next_value=$authenticated ? 0 : $attempts + 1;
    file_put_contents($loginfile, $next_value);
    sleep($delay); // NB this is done regardless if passwd valid
    // you might want to put in your own garbage collection here
 

请注意,正如所写,此过程会泄漏安全信息 - 即,攻击系统的人可能会看到用户何时登录(攻击者尝试的响应时间将降至 0)。您还可以调整算法,以便根据先前的延迟和文件上的时间戳计算延迟。

HTH

C.

【讨论】:

一种更实用的方法是记录所有失败的登录,并查看最近约 10 分钟内的失败尝试次数是否很麻烦,否则攻击者可以只保留交替的用户名。我写了一个为你做这个的课程github.com/ejfrancis/BruteForceBlocker【参考方案11】:

在这种情况下,Cookie 或基于会话的方法当然是无用的。应用程序必须检查以前登录尝试的 IP 地址或时间戳(或两者)。

如果攻击者有多个 IP 来启动他/她的请求,则可以绕过 IP 检查,如果多个用户从同一个 IP 连接到您的服务器,则可能会很麻烦。在后一种情况下,有人多次登录失败会阻止共享同一 IP 的所有人在一段时间内使用该用户名登录。

时间戳检查存在与上述相同的问题:每个人都可以通过多次尝试来阻止其他人登录特定帐户。使用验证码而不是长时间等待最后一次尝试可能是一个很好的解决方法。

登录系统应该防止的唯一额外的事情是尝试检查功能的竞争条件。比如下面的伪代码

$time = get_latest_attempt_timestamp($username);
$attempts = get_latest_attempt_number($username);

if (is_valid_request($time, $attempts)) 
    do_login($username, $password);
 else 
    increment_attempt_number($username);
    display_error($attempts);

如果攻击者同时向登录页面发送请求会发生什么?可能所有请求都会以相同的优先级运行,并且很可能在其他请求超过第二行之前没有请求到达 increment_attempt_number 指令。所以每个请求都会获得相同的 $time 和 $attempts 值并被执行。对于复杂的应用程序而言,防止此类安全问题可能很困难,并且涉及锁定和解锁数据库的某些表/行,这当然会减慢应用程序的速度。

【讨论】:

在 VPS 或共享主机上运行的标准应用程序每秒只能处理大约 5-30 个请求。因此,您的方法确实有效,但在阻止它们之前可能会尝试 30 次。还要检查您的 apache 日志中的此类内容(尤其是发布请求)。【参考方案12】:

cballuo 提供了一个很好的答案。我只是想通过提供支持 mysqli 的更新版本来报答。我稍微改变了 sqls 中的表/字段列和其他小东西,但它应该可以帮助任何寻找 mysqli 等价物的人。

function get_multiple_rows($result) 
  $rows = array();
  while($row = $result->fetch_assoc()) 
    $rows[] = $row;
  
  return $rows;


$throttle = array(10 => 1, 20 => 2, 30 => 5);

$query = "SELECT MAX(time) AS attempted FROM failed_logins";    

if ($result = $mysqli->query($query)) 

    $rows = get_multiple_rows($result);

$result->free();

$latest_attempt = (int) date('U', strtotime($rows[0]['attempted'])); 

$query = "SELECT COUNT(1) AS failed FROM failed_logins WHERE time > DATE_SUB(NOW(), 
INTERVAL 15 minute)";   

if ($result = $mysqli->query($query)) 

$rows = get_multiple_rows($result);

$result->free();

    $failed_attempts = (int) $rows[0]['failed'];

    krsort($throttle);
    foreach ($throttle as $attempts => $delay) 
        if ($failed_attempts > $attempts) 
                echo $failed_attempts;
                $remaining_delay = (time() - $latest_attempt) - $delay;

                if ($remaining_delay < 0) 
                echo 'You must wait ' . abs($remaining_delay) . ' seconds before your next login attempt';
                                

            break;
        
             
  

【讨论】:

我写了一个更新的版本,做成一个类并使用 PDO github.com/ejfrancis/BruteForceBlocker

以上是关于如何在 PHP 中限制用户登录尝试的主要内容,如果未能解决你的问题,请参考以下文章

如何检查用户是不是在 php 中登录?

如何检查用户是不是在 php 中登录?

在C ++中限制登录尝试次数

如何使用 VueJS 限制对未登录用户的页面访问?

使用 PHP 登录后如何显示用户名?

我正在尝试让用户登录 php