【问题标题】:Access logging in PHP在 PHP 中访问日志记录
【发布时间】:2012-02-23 09:45:09
【问题描述】:

我想记录对/files 文件夹中任何文件的访问,所以我可以使用 PHP 对其进行处理以生成一些统计信息。

我不想编写通过 RewriteRule 调用的自定义 PHP 处理程序,因为我不想处理状态代码、MIME 类型和缓存标头以及文件锁定问题。

我无权访问服务器配置,因此无法使用CustomLog(我可以访问.htacess)。

我不能使用X-Sendfile,因为它没有启用。

我无权访问access.log


寻找权威答案。

【问题讨论】:

  • 你可以访问access_log进行解析吗?
  • /files 中的文件是否可下载,或者它们只是提供给用户的文件,如图像、样式表等?
  • 如此多的限制......不值得。如果你想记录你需要控制。
  • 认真的吗?您想要记录,但不想为记录做任何事情,并且您无权访问实际日志。好吧...只需将 index.php?file=12345.txt 从 /files 提高一级并记录某人请求 files/12345.txt 的地方,然后执行 header("Location: files/12345.txt") 重定向。当然,任何想要的人都可以通过转到 files/12345.txt 来绕过您的跟踪,但是......哦,好吧。
  • 如果是 apache mod_php 站点,请尝试 virtual() 函数。它的工作方式类似于 X-Sendfile 标头。见example

标签: php apache logging


【解决方案1】:

这是你设置的很多限制。

您可以使用通过 PHP include 在每个适用(或 __FILE__ 解析,不适用)脚本顶部安装的自定义处理程序来执行此操作。 您必须有一个在每个文件被点击时运行的脚本,并且您已经排除了对服务器配置的更改(包括,我相信,.htaccess 当您说 RewriteRule 不够好时),这意味着您通过基于脚本的看门人来执行此操作。你不能有一个解决方案来满足你的限制,让用户在不首先使用 PHP(或其他服务器端动态语言)的情况下访问文件。可以通过重定向用户到实际文件而不是通过 PHP 运行静态内容来保留缓存。

您可以将日志信息存储在数据库中,或者将文件存储在服务器可写的位置(如果使用文件,请注意争用 - 附加模式很棘手)。

编辑:quickshiftin 指出了两种无需手动添加 include 调用即可调用 PHP 的方法。

【讨论】:

  • A RewriteRule 很好,但我只能看到导致处理程序完整,需要我镜像 MIME 类型的标头和缓存标头(我不想这样做) .
  • 我不一定要寻找 PHP 解决方案。 CustomLog 不是基于 PHP 的(但我不能使用它)。我尽量保持开放的态度,因为我认为我的限制非常严格,我只是觉得奇怪,似乎没有一个简单的解决方案。我不喜欢@quickshiftin 调用 PHP 脚本的方式。
  • @Fritsvacampen 您不喜欢我调用 PHP 脚本的方式,但是您可以按照 yes123 的建议通过 PHP 文件路由所有请求吗?这有什么意义?
  • @quickshiftin - 通过 PHP 路由请求很好,因为我可以控制 PHP 的功能。如果我 执行 任何随机二进制文件作为 PHP,我不知道会发生什么。我确实喜欢 auto_append/prepend 功能 - 我过去曾成功使用过它(用于自动加载内容) - 但它不适用于非 PHP 文件。
【解决方案2】:

创建一个auto_prepend_file 并定义一个函数来记录你想要的。您需要访问 .htaccess 才能设置这些(并且虚拟主机需要类似 AllowOverride all 在虚拟主机中)或使用 PHP 5.3,您可以使用 per-directory INI feature

.htaccess

php_value auto_prepend_file /path/to/file.php

每个目录的 php.ini(PHP 5.3 CGI/Fast CGI SAPI)

user_ini.auto_prepend_file = /path/to/file.php

然后为您的文件 /path/to/file.php (我敢肯定,更优雅的东西;))

file_put_contents(
    LOG_FILE,
    implode(PHP_EOL . PHP_EOL, array(
                'SERVER: ' . PHP_EOL . print_r($_SERVER, true),
                'REQUEST: ' . PHP_EOL . print_r($_REQUEST, true)
            )),
    FILE_APPEND
);

这种方法的美妙之处在于您可能能够摆脱它,并且您只需在一个地方定义/包含日志记录代码。

编辑:

回想起来,我发现您希望这适用于任意类型的文件...是的,这将是相当粗糙的。我能想到的最佳选择是将这些文件标记为 .php 或在 .htaccess 中定义自定义 mime 类型。想法是通过 PHP 解释器运行文件,从而执行 auto_prepend_file 并且由于文件中没有 PHP 标记,因此内容直接发送到客户端。甚至可能在每个设置 ContentType 标头的内容文件的顶部使用一点 PHP。我什至不确定这会奏效,但它可能会。

【讨论】:

  • 他无权访问 .htaccess!
  • 我现在看到他说他无权访问服务器配置。最初我把它作为虚拟主机级别的配置,惊讶 .htaccess 甚至不可用。
  • 我可以访问.htacess,只是不能访问httpd.confphp.ini。据我所知php_auto_prepend 仅适用于 PHP 可执行文件。我想记录“常规”二进制文件。
  • 我对通过 PHP 解析器传递的任何随机文件都不太满意。除了安全问题,这似乎很浪费。
  • @FritsvanCampen 如果这些文件是您放在服务器上的文件,那么它们不是随机的;并且使用我建议的方法,客户永远不会知道其中有 PHP(假设设置 ContentType 工作正常)。此外,如果 PHP 位于任何普通 PHP 文件中都不存在的数据文件之上,会有哪些安全问题?最后,考虑到这种情况下的所有限制,请准备好在性能(和实现优雅)方面做出一些牺牲。
【解决方案3】:

考虑到您不需要限制访问,这很容易做到。

建立一个页面logger.php 接受输入请求的文件,例如:

logger.php?file=abc.exe

logger.php 中,您只需记录此访问,然后重定向到文件:

file_put_contents('log', $_GET['file'] . ' requested',FILE_APPEND);
header('Location: files/'.$_GET['file']);

只需检查$_GET['file'] 是否有恶意文件

当然,您必须替换您网站中的链接,来自:

<a href="files/abc.exe">

<a href="logger.php?file=abc.exe">

【讨论】:

  • 然后客户端必须通过 PHP 脚本访问文件。也许没关系。听起来 OP 希望用户能够直接点击文件并仍然能够登录,但实际上并没有具体的一点。
  • 这解决了必须镜像 MIME-Types 和其他标头的问题,但我不确定对缓存有什么影响。
  • 我想我可以测试 301 Location 标头的效果。
  • 效果?这只是一个重定向oO
  • 一个简单的谷歌搜索会告诉你 301 重定向对于每个搜索引擎来说都非常合适。甚至谷歌也建议你尽可能使用 301 重定向。
【解决方案4】:

这里的意图似乎是绕过 Apache 和 PHP 中固有的所有系统。如果您的服务器实例上确实存在这些限制,那么您最好要求更改您的权限,而不是设计一个系统管理员可能对您实施感到满意或不满意的解决方法。

【讨论】:

  • 如果您获得任何不是自我管理服务器的托管计划,这些都是您使用的限制。所以我们中大约 99% 的人都在处理这些限制。我相信我已经充分解释了为什么我不能使用某些解决方案。
  • 我了解,但是出于非常具体的原因,这些限制在这些环境中存在。尝试做一些服务器管理员似乎非常明确地不希望你做的事情让我觉得这是个坏主意。
  • 是的,但我不认为日志记录是管理员不希望我做的事情。只是CustomLogserver config 级别设置(我认为这是一个错误)。我不认为 Apache 在编写时考虑了“通过功能匮乏的安全性”。阻止对server config 的访问是保护服务器的一种非常方便的方法:P
  • 是的,这就是为什么我建议您联系您的管理员。我过去也遇到过这样的问题,在大多数情况下,当您指出限制不合理时,您可以稍微放松一下安全带。不要忘记奥卡姆剃刀:)
  • 我认为鉴于 Apache 和 PHP 之间的逻辑分离以及您所处的限制性情况,您唯一合乎逻辑的选择是您已经排除的选择;自定义处理程序脚本。由于那里的问题是处理所有可能出现的问题,您是否研究过现有的解决方案,例如zubrag.com/scripts/download.php
【解决方案5】:

可能不是您想要的,但您为什么不完全使用不同的解决方案?

您可以使用 Google Analytics VirtualPageviews 通过 Javascript 跟踪文件下载。

更多信息请看这里:http://support.google.com/googleanalytics/bin/answer.py?hl=en&answer=55529

您甚至可以创建自己的 JS 来通过浏览器跟踪文件下载,而无需费心 GA。

更新

正如我所说,您可以轻松创建自己的 JS 来跟踪它们,而无需使用 GA。这是 jQuery 中一个可行的愚蠢示例(尚未对其进行测试 - 只是在我的头顶上写了它):

代码示例:

JS端:

$(document).ready(function() {
  $("a").click(function() {
    if( $(this).attr('href').match(/\/files\/(.*)/) ) {
      $.ajax({
        url: '/tracking/the/file/downloads.php'
        data: {
          'ok': 'let\'s',
          'add': 'some information',
          'about': 'the user that initiated',
          'the': 'request',
          'file': $(this).attr('href')
        }
      });
    }

    return true;
  });
});

【讨论】:

  • 我已经考虑过这一点,但我不能真正使用它,因为我希望数据随时可用。我知道有用于 Google Analytics 的 API(我将这些 API 用于常规页面查看),但它不适合我在这里尝试做的事情。
  • @FritsvanCampen 检查我更新的答案。没有必要和GA一起去。您可以手动执行此操作,并让您的数据立即可用。
  • 我想这也可以。我确实看到了一些问题:首先,无论缓存如何,每个请求都有一些开销。其次,如果您正在处理 ajax-ed 内容,则需要再次触发此处理程序。如果文件使用来自其他域或不包含跟踪代码的页面,则您无法捕获它们。最后,你对 JavaScript 有依赖,——我并不关心这个——但它可能是相关的。
  • @FritsvanCampen 确实有额外的要求,但这可以进行大量优化。您甚至可以跳过 AJAX 请求并使用 nginx 1x1 魔术图像 (wiki.nginx.org/HttpEmptyGifModule) 使其工作并从那里获取日志。 (这将是超快的)这将需要一个单独的服务器,是的,但另一方面它不会计入浏览器发出的最大 Web 请求。同样使用 jquery,事件被链接起来。因此,如果您要在所有 标记上都有一个 click() 侦听器,那么无论您在链接上绑定了哪些其他点击,它都会触发。
【解决方案6】:

仅适用于 mod_php 情况。有一些性能损失 - apache_lookup_uri() 执行额外的 apache 内部子请求。

正如其他人指出的那样,您需要 .htaccess

RewriteEngine On
RewriteRule ^/handler.php$ - [L]
RewriteRule ^/([a-zA-Z0-9\.]+)$ /handler.php?filename=$1 [L]

在 handler.php 文件中使用 virtual() 函数来执行 apache 子请​​求。 此处示例:http://www.php.net/manual/en/function.virtual.php#88722

更新和测试(但相当少)的解决方案:

<?php
//add some request logging here
$file = $_GET["filename"];

$file_info = apache_lookup_uri($file);
header('content-type: ' . $file_info -> content_type);
// add other headers?
virtual($file);
exit(0);
?>

【讨论】:

  • virtual 似乎没有发送正确的标题,请参阅我的“答案”
  • 我更正了这段代码以获取(通过 apache_lookup_uri)并发送正确的标头。
  • 这可能正是我需要的!
【解决方案7】:

好的,这是一个想法。请耐心等待,起初可能看起来不合适,但请阅读最后的内容。希望它适用于您所拥有的。在包含文件的文件夹中,放置一个 .htaccess,它将所有请求重写到同一目录中的 PHP 处理程序脚本,如下所示(未经测试):

RewriteEngine On
RewriteRule ^/handler.php$ - [L]
RewriteRule ^/([a-zA-Z0-9\.]+)$ /handler.php?filename=$1 [L]

在 PHP 脚本中,您可以使用 file_put_contents() 进行任何必要的日志记录。然后,您使用以下代码创建 handler.php:

<?php
if (!file_exists) {
    header("Status: 404 Not Found");
    //if you have a 404 error page, you can use an include here to show it
    exit(0);
}

header("Content-disposition: attachment; filename={$_GET["filename"]}");
header("Content-type: ".get_mime_type($_GET["filename"]));
readfile($filename);

function get_mime_type($filename, $mimePath = '/etc') {
    $fileext = substr(strrchr($filename, '.'), 1);
    if (empty($fileext)) return (false);
    $regex = "/^([\w\+\-\.\/]+)\s+(\w+\s)*($fileext\s)/i";
    $lines = file("$mimePath/mime.types");
    foreach($lines as $line) {
        if (substr($line, 0, 1) == '#') continue; // skip comments
        $line = rtrim($line) . " ";
        if (!preg_match($regex, $line, $matches)) continue; // no match to the extension
        return ($matches[1]);
    }
    return (false); // no match at all
}
?>

基本上,您是在文件请求和文件的实际服务之间创建一个层。这个 PHP 层记录文件访问,然后提供文件。您说您不想乱用状态代码和 MIME 类型,但这样做的美妙之处在于所有这些都得到了照顾。如果文件不存在,它只会生成标准 404,您可以包含自定义 404 错误页面。是的,这里正在更改状态标题,但这并不复杂。至于 MIME 类型,根据 Apache 使用的相同 MIME 类型规则为您检测它们。将 get_mime_type 函数指向服务器上的 mime.types 文件。如果您不知道它在哪里,只需从here 下载一份副本。我承认,这个解决方案可能比您所寻找的更具技术性,但是由于您的限制,它是一个很好的解决方案。最好的部分是,它对最终用户以及上传内容的人完全透明。

【讨论】:

  • 正是想要的那种处理程序。我没有mime.types 文件的路径。您没有输出任何缓存标头,也没有 ETag。 Content-disposition: attachment 标头也是错误的,因为它强制浏览器下载文件。而file_put_contents,即使有FILE_APPEND 标志,也不是原子的——但我想有办法绕过它。
【解决方案8】:

在不通过 PHP 过滤内容的情况下,您唯一可以做的不显眼的监控是检查所有文件并在每次请求任何 PHP 文件时记下它们的file access times(您只需将一个函数添加到您的 php 文件或使用重写)。它会产生一些开销,但这是您可以获得的唯一不显眼的统计数据。

显然,这种方式无法获得准确的访问次数,但更像是频率,因此它也是某种(可行的)统计数据。要获得诸如命中数之类的信息(它在 3 月 25 日凌晨 2 点打开了 1000k 次),您需要访问日志或通过 PHP 或 cgi 脚本将其全部通过管道传输——只需手动计数即可。

【讨论】:

  • 有趣。我预见的唯一问题是在我的应用程序后端使用文件浏览器也可能“触摸”文件,因此fileatime 将不准确。它的级别太低(操作系统级别)。
【解决方案9】:

假设您使用 PHP 作为编译的 Apache 模块,那么 virtual() 函数可以实现这一点。见:http://www.php.net/manual/en/function.virtual.php

<?php

$fn = $_GET['fn'];

log_file_access($fn); // You define how you want this to happen    
virtual($fn);

然后您可以通过以下方式引用文件:

http://example.com/file.php?fn=files/lolcat.jpg

【讨论】:

  • 这很有趣,但是引用 php 手册中的一位用户 cmets:“下面发布的大多数脚本的问题是 virtual() 在发出子请求之前会刷新待处理的标头。使用 virtual( ) 仍然返回一个 text/html 类型的文档。”
  • virtual 似乎没有发送正确的标题,请参阅我的“答案”。
【解决方案10】:

我尝试了很多东西,但似乎没有简单的解决方案。

我的解决方案使用@yes123 提出的Location 标头技巧,但我已对其进行了调整以符合我的偏好。

文件的链接保持不变,所以它仍然是:/files/path/to/my/file.abc 我有一个RewriteRule

RewriteRule ^files/(.*) path/to/tracker.php?path=/$1

然后在文件中,我通过在 URL 中添加 ?track=no 和之前的 RewriteRule 的例外来发出 Location 标头:

RewriteCond %{QUERY_STRING} !(&amp;|^)track=no(&amp;|$)

我又添加了一项优化。我已启用电子标签,因此如果客户端发送电子标签标头,请查看它是否与文件匹配并返回 304 Not Modified 而不是 Location

$fs = stat($document_root . $path);
$apache_etag = calculate_apache_etag($fs);
if ((isset($_SERVER["HTTP_IF_MATCH"]) && etag_within_range($_SERVER["HTTP_IF_MATCH"], $apache_etag))
    || (isset($_SERVER["HTTP_IF_NONE_MATCH"]) && etag_within_range($_SERVER["HTTP_IF_NONE_MATCH"], $apache_etag))
) {
    header("ETag: " . $apache_etag, true, 304);
    exit;
}

function etag_within_range($etag1, $etag2) {
    list($size1, $mtime1) = explode("-", $etag1);
    list($size2, $mtime2) = explode("-", $etag2);
    $mtime1 = floor(hexdec($mtime1) / 1000000);
    $mtime2 = floor(hexdec($mtime2) / 1000000);
    return $mtime1 === $mtime2 && $size1 === $size2;
}

calculate_apache_etag 的实现可以在这里找到:How do you make an etag that matches Apache?

etag_withing_range 解决了在 Apache 中与更高精度的mtime 进行比较的问题。


关于无效解决方案的说明

virtual

测试脚本:

var_dump(apache_response_headers());
virtual("/path/to/image.jpg");
var_dump(apache_response_headers());

输出:

array(1) { ["X-Powered-By"]=> string(10) "PHP/5.2.11" }
[[binary junk]]
array(5) { ["X-Powered-By"]=> string(10) "PHP/5.2.11" ["Keep-Alive"]=> string(18) "timeout=5, max=100" ["Connection"]=> string(10) "Keep-Alive" ["Transfer-Encoding"]=> string(7) "chunked" ["Content-Type"]=> string(9) "text/html" }

Content-Type: text/htmlreaaaaalllly? :(

也许PHP5.3的header_remove函数可以解决这个问题?我没试过。

【讨论】:

  • 你应该把你的赏金分配给某人
猜你喜欢
  • 1970-01-01
  • 2012-11-07
  • 1970-01-01
  • 2012-08-28
  • 2019-06-12
  • 1970-01-01
  • 2014-06-13
  • 1970-01-01
  • 2020-12-14
相关资源
最近更新 更多