如何检测虚假用户(网络爬虫)和 cURL

41

一些其他的网站使用cURL和虚假的http referer来复制我的网站内容。我们有没有办法检测cURL或者不是真正的Web浏览器?


1
你不能假设网络爬虫无法运行JavaScript...存在着像rhino这样的工具,可以在运行JavaScript的同时对您的站点进行抓取。除非您将内容放置在数字墙后面(登录、认证等),否则它将可供抓取。版权所有材料,如果未经书面许可发布,则起诉他们。如果他们在另一个国家,则祝好运。 - Yzmir Ramirez
1
我不确定这个问题在未来是否会改变,但是cURL(至少是PHP cURL)会忽略Connection: close HTTP响应头。基于此,你最好检测非标准的HTTP客户端(浏览器通常在头部方面遵循大多数RFC标准)。另一个技巧是使用JavaScript片段检测键盘、鼠标和滚动事件,然后将其发送到主页并“验证”当前会话。你甚至可以向当前用户显示对话框 : )。机器人永远不会为它生成点击事件,特别是如果你随机放置它。 - oxygen
3
事实不正确。libcurl(因此也包括PHP / CURL)不会忽略“Connection: close”头。请参阅libcurl源代码中的lib / http.c。 - Daniel Stenberg
@DanielStenberg 我只是从经验谈起,我不会看源代码。 - oxygen
23
我是libcurl的作者,所以我在谈论那段代码。 - Daniel Stenberg
2
@DanielStenberg 我写了一个小脚本来测试 PHP 中的当前 cURL,你是正确的。抱歉 :) - oxygen
6个回答

100

避免自动爬取没有什么魔法解决方案。人类能做的事情,机器人也能做到。只有让工作变得更加困难的解决方案,才会使只有强大技术极客们尝试去通过。

几年前我也遇到了麻烦,我的第一个建议是,如果你有时间,自己成为一个爬虫(我假设“爬虫”就是爬取你网站的那个人),这是学习这个主题的最好方法。通过爬取几个网站,我学到了不同种类的保护措施,并将它们结合起来,使我很有效率。

下面给你一些你可以尝试的保护措施的例子。


每个IP的会话数

如果一个用户每分钟使用50个新会话,你可能会认为这个用户可能是一个不处理cookie的爬虫。当然,curl完美地处理cookies,但是如果你将其与每个会话的访问计数器(稍后解释)结合使用,或者你的爬虫在cookie问题上是个新手,这可能是有效的。

很难想象有50个来自同一个共享连接的人会同时进入你的网站(当然这取决于你的流量,这由你来决定)。如果这种情况发生,你可以锁定你网站的页面,直到填写验证码为止。

想法:

1)你创建2张表:一张用于保存被禁止的IP地址,另一张用于保存IP地址和会话

create table if not exists sessions_per_ip (
  ip int unsigned,
  session_id varchar(32),
  creation timestamp default current_timestamp,
  primary key(ip, session_id)
);

create table if not exists banned_ips (
  ip int unsigned,
  creation timestamp default current_timestamp,
  primary key(ip)
);

2) 在您的脚本开头,从两个表中删除过时的条目

3) 接下来,检查您的用户的ip地址是否被禁止(将标志设置为true)

4) 如果没有被禁止,计算他的ip地址有多少会话

5) 如果他的会话太多,将其插入到被禁止表中并设置标志

6) 如果还没有插入,将其ip地址插入到每个ip地址的会话表中

我编写了一个代码示例来更好地展示我的想法。

<?php

try
{

    // Some configuration (small values for demo)
    $max_sessions = 5; // 5 sessions/ip simultaneousely allowed
    $check_duration = 30; // 30 secs max lifetime of an ip on the sessions_per_ip table
    $lock_duration = 60; // time to lock your website for this ip if max_sessions is reached

    // Mysql connection
    require_once("config.php");
    $dbh = new PDO("mysql:host={$host};dbname={$base}", $user, $password);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // Delete old entries in tables
    $query = "delete from sessions_per_ip where timestampdiff(second, creation, now()) > {$check_duration}";
    $dbh->exec($query);

    $query = "delete from banned_ips where timestampdiff(second, creation, now()) > {$lock_duration}";
    $dbh->exec($query);

    // Get useful info attached to our user...
    session_start();
    $ip = ip2long($_SERVER['REMOTE_ADDR']);
    $session_id = session_id();

    // Check if IP is already banned
    $banned = false;
    $count = $dbh->query("select count(*) from banned_ips where ip = '{$ip}'")->fetchColumn();
    if ($count > 0)
    {
        $banned = true;
    }
    else
    {
        // Count entries in our db for this ip
        $query = "select count(*)  from sessions_per_ip where ip = '{$ip}'";
        $count = $dbh->query($query)->fetchColumn();
        if ($count >= $max_sessions)
        {
            // Lock website for this ip
            $query = "insert ignore into banned_ips ( ip ) values ( '{$ip}' )";
            $dbh->exec($query);
            $banned = true;
        }

        // Insert a new entry on our db if user's session is not already recorded
        $query = "insert ignore into sessions_per_ip ( ip, session_id ) values ('{$ip}', '{$session_id}')";
        $dbh->exec($query);
    }

    // At this point you have a $banned if your user is banned or not.
    // The following code will allow us to test it...

    // We do not display anything now because we'll play with sessions :
    // to make the demo more readable I prefer going step by step like
    // this.
    ob_start();

    // Displays your current sessions
    echo "Your current sessions keys are : <br/>";
    $query = "select session_id from sessions_per_ip where ip = '{$ip}'";
    foreach ($dbh->query($query) as $row) {
        echo "{$row['session_id']}<br/>";
    }

    // Display and handle a way to create new sessions
    echo str_repeat('<br/>', 2);
    echo '<a href="' . basename(__FILE__) . '?new=1">Create a new session / reload</a>';
    if (isset($_GET['new']))
    {
        session_regenerate_id();
        session_destroy();
        header("Location: " . basename(__FILE__));
        die();
    }

    // Display if you're banned or not
    echo str_repeat('<br/>', 2);
    if ($banned)
    {
        echo '<span style="color:red;">You are banned: wait 60secs to be unbanned... a captcha must be more friendly of course!</span>';
        echo '<br/>';
        echo '<img src="http://4.bp.blogspot.com/-PezlYVgEEvg/TadW2e4OyHI/AAAAAAAAAAg/QHZPVQcBNeg/s1600/feu-rouge.png" />';
    }
    else
    {
        echo '<span style="color:blue;">You are not banned!</span>';
        echo '<br/>';
        echo '<img src="http://identityspecialist.files.wordpress.com/2010/06/traffic_light_green.png" />';
    }
    ob_end_flush();
}
catch (PDOException $e)
{
    /*echo*/ $e->getMessage();
}

?>

访问计数器

如果用户使用相同的cookie来爬取您的页面,您将能够利用他的会话来阻止它。这个想法很简单:您的用户是否可能在60秒内访问60个页面?

思路:

  1. 在用户会话中创建一个数组,它将包含访问时间(time())。
  2. 删除此数组中早于X秒的访问
  3. 为实际访问添加新条目
  4. 计算此数组中的条目数
  5. 如果用户访问了Y个页面,则禁止该用户

示例代码:

<?php

$visit_counter_pages = 5; // maximum number of pages to load
$visit_counter_secs = 10; // maximum amount of time before cleaning visits

session_start();

// initialize an array for our visit counter
if (array_key_exists('visit_counter', $_SESSION) == false)
{
    $_SESSION['visit_counter'] = array();
}

// clean old visits
foreach ($_SESSION['visit_counter'] as $key => $time)
{
    if ((time() - $time) > $visit_counter_secs) {
        unset($_SESSION['visit_counter'][$key]);
    }
}

// we add the current visit into our array
$_SESSION['visit_counter'][] = time();

// check if user has reached limit of visited pages
$banned = false;
if (count($_SESSION['visit_counter']) > $visit_counter_pages)
{
    // puts ip of our user on the same "banned table" as earlier...
    $banned = true;
}

// At this point you have a $banned if your user is banned or not.
// The following code will allow us to test it...

echo '<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>';

// Display counter
$count = count($_SESSION['visit_counter']);
echo "You visited {$count} pages.";
echo str_repeat('<br/>', 2);

echo <<< EOT

<a id="reload" href="#">Reload</a>

<script type="text/javascript">

  $('#reload').click(function(e) {
    e.preventDefault();
    window.location.reload();
  });

</script>

EOT;

echo str_repeat('<br/>', 2);

// Display if you're banned or not
echo str_repeat('<br/>', 2);
if ($banned)
{
    echo '<span style="color:red;">You are banned! Wait for a short while (10 secs in this demo)...</span>';
    echo '<br/>';
    echo '<img src="http://4.bp.blogspot.com/-PezlYVgEEvg/TadW2e4OyHI/AAAAAAAAAAg/QHZPVQcBNeg/s1600/feu-rouge.png" />';
}
else
{
    echo '<span style="color:blue;">You are not banned!</span>';
    echo '<br/>';
    echo '<img src="http://identityspecialist.files.wordpress.com/2010/06/traffic_light_green.png" />';
}
?>

一个可下载的图片

当爬虫需要处理大量数据并在最短时间内完成任务时,它们通常不会下载页面上的图片,因为这会占用太多带宽并降低爬取速度。

这个想法(我认为是最优雅且最易实现的)使用mod_rewrite将代码隐藏在 .jpg/.png/... 图片文件中。该图片应该在你要保护的每个页面上都可用:可以选择你网站的标志作为该图片,但你需要选择小尺寸的图片(因为此图片不应被缓存)。

操作步骤:

1/ 在你的 .htaccess 文件中添加以下行:

RewriteEngine On
RewriteBase /tests/anticrawl/
RewriteRule ^logo\.jpg$ logo.php

2/ 使用安全方式创建您的 logo.php

<?php

// start session and reset counter
session_start();
$_SESSION['no_logo_count'] = 0;

// forces image to reload next time
header("Cache-Control: no-store, no-cache, must-revalidate");

// displays image
header("Content-type: image/jpg");
readfile("logo.jpg");
die();

3/ 在需要添加安全性的每个页面上,增加 no_logo_count 的值,并检查是否达到了限制。

示例代码:

<?php

$no_logo_limit = 5; // number of allowd pages without logo

// start session and initialize
session_start();
if (array_key_exists('no_logo_count', $_SESSION) == false)
{
    $_SESSION['no_logo_count'] = 0;
}
else
{
    $_SESSION['no_logo_count']++;
}

// check if user has reached limit of "undownloaded image"
$banned = false;
if ($_SESSION['no_logo_count'] >= $no_logo_limit)
{
    // puts ip of our user on the same "banned table" as earlier...
    $banned = true;
}

// At this point you have a $banned if your user is banned or not.
// The following code will allow us to test it...

echo '<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>';

// Display counter
echo "You did not loaded image {$_SESSION['no_logo_count']} times.";
echo str_repeat('<br/>', 2);

// Display "reload" link
echo <<< EOT

<a id="reload" href="#">Reload</a>

<script type="text/javascript">

  $('#reload').click(function(e) {
    e.preventDefault();
    window.location.reload();
  });

</script>

EOT;

echo str_repeat('<br/>', 2);

// Display "show image" link : note that we're using .jpg file
echo <<< EOT

<div id="image_container">
    <a id="image_load" href="#">Load image</a>
</div>
<br/>

<script type="text/javascript">

  // On your implementation, you'llO of course use <img src="logo.jpg" />
  $('#image_load').click(function(e) {
    e.preventDefault();
    $('#image_load').html('<img src="logo.jpg" />');
  });

</script>

EOT;

// Display if you're banned or not
echo str_repeat('<br/>', 2);
if ($banned)
{
    echo '<span style="color:red;">You are banned: click on "load image" and reload...</span>';
    echo '<br/>';
    echo '<img src="http://4.bp.blogspot.com/-PezlYVgEEvg/TadW2e4OyHI/AAAAAAAAAAg/QHZPVQcBNeg/s1600/feu-rouge.png" />';
}
else
{
    echo '<span style="color:blue;">You are not banned!</span>';
    echo '<br/>';
    echo '<img src="http://identityspecialist.files.wordpress.com/2010/06/traffic_light_green.png" />';
}
?>

检查Cookie

你可以在JavaScript中创建cookie来检查用户是否能够解释JavaScript(例如使用Curl的爬虫就不能)。

这个想法非常简单:与图像检查大致相同。

  1. 将 $_SESSION 的值设置为 1,并在每次访问时增加它
  2. 如果存在一个在JavaScript中设置的cookie,则将session值设置为0
  3. 如果此值达到限制,则封禁用户

代码:

<?php

$no_cookie_limit = 5; // number of allowd pages without cookie set check

// Start session and reset counter
session_start();

if (array_key_exists('cookie_check_count', $_SESSION) == false)
{
    $_SESSION['cookie_check_count'] = 0;
}

// Initializes cookie (note: rename it to a more discrete name of course) or check cookie value
if ((array_key_exists('cookie_check', $_COOKIE) == false) || ($_COOKIE['cookie_check'] != 42))
{
    // Cookie does not exist or is incorrect...
    $_SESSION['cookie_check_count']++;
}
else
{
    // Cookie is properly set so we reset counter
    $_SESSION['cookie_check_count'] = 0;
}

// Check if user has reached limit of "cookie check"
$banned = false;
if ($_SESSION['cookie_check_count'] >= $no_cookie_limit)
{
    // puts ip of our user on the same "banned table" as earlier...
    $banned = true;
}

// At this point you have a $banned if your user is banned or not.
// The following code will allow us to test it...

echo '<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>';

// Display counter
echo "Cookie check failed {$_SESSION['cookie_check_count']} times.";
echo str_repeat('<br/>', 2);

// Display "reload" link
echo <<< EOT

<br/>
<a id="reload" href="#">Reload</a>
<br/>

<script type="text/javascript">

  $('#reload').click(function(e) {
    e.preventDefault();
    window.location.reload();
  });

</script>

EOT;

// Display "set cookie" link
echo <<< EOT

<br/>
<a id="cookie_link" href="#">Set cookie</a>
<br/>

<script type="text/javascript">

  // On your implementation, you'll of course put the cookie set on a $(document).ready()
  $('#cookie_link').click(function(e) {
    e.preventDefault();
    var expires = new Date();
    expires.setTime(new Date().getTime() + 3600000);
    document.cookie="cookie_check=42;expires=" + expires.toGMTString();
  });

</script>
EOT;


// Display "unset cookie" link
echo <<< EOT

<br/>
<a id="unset_cookie" href="#">Unset cookie</a>
<br/>

<script type="text/javascript">

  // On your implementation, you'll of course put the cookie set on a $(document).ready()
  $('#unset_cookie').click(function(e) {
    e.preventDefault();
    document.cookie="cookie_check=;expires=Thu, 01 Jan 1970 00:00:01 GMT";
  });

</script>
EOT;

// Display if you're banned or not
echo str_repeat('<br/>', 2);
if ($banned)
{
    echo '<span style="color:red;">You are banned: click on "Set cookie" and reload...</span>';
    echo '<br/>';
    echo '<img src="http://4.bp.blogspot.com/-PezlYVgEEvg/TadW2e4OyHI/AAAAAAAAAAg/QHZPVQcBNeg/s1600/feu-rouge.png" />';
}
else
{
    echo '<span style="color:blue;">You are not banned!</span>';
    echo '<br/>';
    echo '<img src="http://identityspecialist.files.wordpress.com/2010/06/traffic_light_green.png" />';
}

防范代理

关于网络中不同类型的代理,我们需要了解以下几点:

  • “普通”代理会显示用户连接相关信息(尤其是IP地址)
  • 匿名代理不显示IP地址,但在标头中提供有关代理使用的信息。
  • 高度匿名代理不显示用户IP,并且不显示任何浏览器可能未发送的信息。

虽然很容易找到代理连接任何网站,但很难找到高度匿名的代理。

一些 $_SERVER 变量可能包含特定键,特别是当您的用户在代理后面时(从此问题中获取详细列表),例如:

  • CLIENT_IP
  • FORWARDED
  • FORWARDED_FOR
  • FORWARDED_FOR_IP
  • HTTP_CLIENT_IP
  • HTTP_FORWARDED
  • HTTP_FORWARDED_FOR
  • HTTP_FORWARDED_FOR_IP
  • HTTP_PC_REMOTE_ADDR
  • HTTP_PROXY_CONNECTION'
  • HTTP_VIA
  • HTTP_X_FORWARDED
  • HTTP_X_FORWARDED_FOR
  • HTTP_X_FORWARDED_FOR_IP
  • HTTP_X_IMFORWARDS
  • HTTP_XROXY_CONNECTION
  • VIA
  • X_FORWARDED
  • X_FORWARDED_FOR

如果在您的 $_SERVER 变量中检测到其中之一的键,则可以为反爬虫安全性设置不同的行为(例如降低限制等)。


结论

有许多方法可以检测网站上的滥用行为,因此您一定能找到解决方案。但是,您需要准确了解您的网站如何被使用,以便您的安全措施不会对“正常”用户造成影响。


1
我喜欢你在Javascript中使用set cookie,然后使用PHP读取它并进行BAN。 - Ken Le
Pinterest.com尝试了这一切,但我仍然成功地完成了它 ;) -- 代码http://stackoverflow.com/questions/28629343/why-isnt-curl-logging-into-external-website/28660511#28660511 - hanshenrik
所有的技术狂人都可以通过任何保护措施,甚至是验证码 :-). 我认为最有效的保护措施是图像和编码 JavaScript 中的保护措施。 - Alain Tiemblo
非常感谢您提供如此详尽的答案。我强烈建议您研究一下转义发送到查询的参数或使用预处理语句来避免 SQL 注入。谢谢。 - Svetoslav Marinov

2
您可以通过以下方法检测cURL-Useragent。但请注意,用户代理可能会被用户覆盖,无论如何,可以通过以下方式识别默认设置:
function is_curl() {
    if (stristr($_SERVER["HTTP_USER_AGENT"], 'curl'))
        return true;
}

2
记住:HTTP不是魔法。每个HTTP请求都会发送一组定义好的头信息;如果这些头信息能够被网页浏览器发送,那么任何程序也都可以发送 - 包括cURL(和libcurl)。
有些人认为这是一种诅咒,但另一方面,这也是一种福音,因为它极大地简化了Web应用程序的功能测试。
更新:正如unr3al011所指出的那样,curl不能执行JavaScript,因此从理论上讲,可能会创建一个页面,在抓取器(例如通过JS手段设置和稍后检查特定cookie)查看时会有不同的行为。
不过,这将是一种非常脆弱的防御措施。页面的数据仍然必须从服务器中获取 - 而且这个HTTP请求(它总是HTTP请求)可以被cURL模拟。请参见this answer,了解如何击败这样的防御措施。
......我甚至没有提到一些抓取工具能够执行JavaScript的事实。)

现在cURL可以设置用户代理和HTTP引荐,那么我们根本无法检测到它吗? - Ken Le
不。我会说“不幸的是”,但又想想,事实并非如此:如果curl不能发送它,任何其他库都会取代它。 - raina77ow
你可以检测curl。如果你假设一个请求来自curl,那么就检查它是否能够执行Javascript。Curl无法执行Javascript。 - pila
1
如何在 PHP 代码中检测“无法执行 JavaScript”? - Ken Le
1
@KenLe 你有没有看到我提到的答案?它包含了既有 HTML 和 JS 检查,也有 PHP 代码来打败它。我觉得在这里再加上这段代码没有太大意义,因为它并不是一个解决方案。 - raina77ow

0

避免虚假引荐的方法是跟踪用户

您可以通过以下一种或多种方法跟踪用户:

  1. 在浏览器客户端中保存一个带有特殊代码(例如:最后访问的URL,时间戳)的cookie,并在服务器每次响应时进行验证。

  2. 与上述相同,但使用会话而不是显式cookie。

对于cookie,您应该添加加密安全性。

[Cookie]
url => http://someurl/
hash => dsafdshfdslajfd

在PHP中,哈希是通过以下方式计算的

$url = $_COOKIE['url'];
$hash = $_COOKIE['hash'];
$secret = 'This is a fixed secret in the code of your application';

$isValidCookie = (hash('algo', $secret . $url) === $hash);

$isValidReferer = $isValidCookie & ($_SERVER['HTTP_REFERER'] === $url)

1
这是一个基础桩代码,您应该根据自己的需求进行改进。 - Maks3w

-1

正如一些人提到的那样,cURL无法执行JavaScript(据我所知),因此您可以尝试设置像raina77ow建议的东西,但这对其他抓取器/下载器不起作用。

我建议您尝试构建一个机器人陷阱,这样您就可以处理能够执行JavaScript的抓取器/下载器。

我不知道任何完全防止这种情况的解决方案,因此我最好的建议是尝试多种解决方案:

1)仅允许已知用户代理,例如所有主流浏览器在您的.htaccess文件中

2)设置您的robots.txt以防止机器人

3)为不遵守robots.txt文件的机器人设置机器人陷阱


-1 表示“1)在您的 .htaccess 文件中拒绝任何未知的用户代理”。 - oxygen
我的意思是只允许已知的用户代理,比如所有主流浏览器,这样如果有人使用不同的用户代理来抓取数据,他们将被拒绝。 - Rayvyn
你的意思和现在所说的完全不同。 - oxygen
关于1):此外,更改cURL“广播”的用户代理非常容易,还请参阅Marcel Gent Simonis答案下的评论。 - Benjamin Seiller
小心不要阻止搜索引擎...它们的爬虫可以轻松更改用户代理。 - mowgli

-5
将以下内容作为.htaccess文件放入根目录中,可能会有所帮助。我在一个网络托管提供商的网站上找到了这个,但不知道它是什么意思 :)
SetEnvIf User-Agent ^Teleport graber   
SetEnvIf User-Agent ^w3m graber    
SetEnvIf User-Agent ^Offline graber   
SetEnvIf User-Agent Downloader graber  
SetEnvIf User-Agent snake graber  
SetEnvIf User-Agent Xenu graber   
Deny from env=graber

9
你确定要发布你不知道意思的解决方案吗? - eis
你可以自己找到这句话的含义,我只是复制了它。他们说用这个会很难抓取你的网页。我认为这对于一些抓取工具来说是限制,正如你所看到的。 - GentSVK
4
它的作用是接收指定的用户代理字符串部分,将它们定义为“graber”环境并拒绝访问。如果在用户代理中没有使用这些部分,则不会有任何操作。 - eis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接