使用PHP/Apache来限制对静态文件(html、css、img等)的访问

41

假设你的服务器上有许多 html、css、js、img 等文件,存在一个目录中。通常情况下,通过输入完整的 URL(例如:http://example.com/static-files/sub/index.html),任何互联网用户都可以访问这些文件。

现在,如果你只想让授权用户能够加载这些文件呢?以本例为例,假设你的用户先从以下 URL 登录:http://example.com/login.php

那么,如何允许已登录的用户查看 index.html 文件(或“static-files”下的任何文件),但将该文件限制为其他所有人无法访问呢?

迄今为止,我想出了两种可能的解决方案:

解决方案1
在“static-files”下创建以下 .htaccess 文件:

Options +FollowSymLinks  
RewriteEngine on  
RewriteRule ^(.*)$ ../authorize.php?file=$1 [NC]

然后在 authorize.php 文件中...

if (isLoggedInUser()) readfile('static-files/'.$_REQUEST['file']);
else echo 'denied';

这个 authorize.php 文件被严重简化了,但是你可以得到其中的思路。

解决方法 2
在“静态文件”文件夹下创建以下 .htaccess 文件:

Order Deny,Allow
Deny from all
Allow from 000.000.000.000
然后我的登录页面可以附加每个登录用户的IP地址到.htaccess文件中。显然,这也需要一些清理例程来清除旧的或不再使用的IP地址。 我担心随着用户数量和他们访问的文件数量增加,我的第一个解决方案可能会在服务器上变得非常昂贵。我认为我的第二个解决方案会便宜得多,但由于IP欺骗等原因,也更不安全。我还担心如果有许多同时在线的用户,将这些IP地址写入htaccess文件可能会成为应用程序的瓶颈。
哪种解决方案听起来更好,为什么?或者,您能想到完全不同的解决方案比这两个都更好吗?

1
使用亚马逊S3服务怎么样? - sonam
9个回答

37
我建议使用PHP加载器处理身份验证并返回所需文件。例如,不要使用<img src='picture.jpg' />,而应该使用<img src='load_image.php?image=picture.jpg' />。
您的图像加载程序可以验证会话、检查凭据等,然后决定是否将所请求的文件返回给浏览器。这将使您能够在网络可访问的根目录之外存储所有安全文件,因此没有人会通过WGET或“意外”浏览到它们。
只需在PHP中返回正确的标头,并执行类似于readfile()的操作即可将文件内容返回给浏览器。
我已经在几个大规模安全网站上使用过这种设置,效果非常好。
编辑:我目前正在构建的系统使用此方法来加载JavaScript、图像和视频,但我们不太担心保护CSS。

2
谢谢Shane。这个加载器基本上就是我在第一个解决方案中描述的。我对它的担忧是服务器费用,但很高兴听到它在你的“大规模安全网站”中运行良好。 - Bart
我通常不使用你上面提到的.htaccess规则(虽然我认为没有理由不使用),而是创建一些包装函数来构建正确的查询字符串并传递参数。由于您的服务器通常会返回二进制数据,因此唯一的额外开销是PHP处理,这对我来说非常值得,特别是与.htaccess开销相比,后者需要检查系统中每个页面请求的许多(数百|数千)个IP地址。我认为这只是众多解决方法之一!如果您发现其他方法,请告诉我! - Shane
在这种情况下,可以说浏览器无法缓存任何图像吗? - Lotus Notes
1
不一定。在PHP中,实际上可以设置自己的标头(包括缓存处理标头)。您还可以检查从浏览器发送给您的标头以确定响应。这比某些人想要涉足的要复杂一些,但是您可以获得更多的控制权。我确实发送缓存控制标头,以便可以非常严格地控制缓存。 - Shane
@Shanee,你怎么样才能防止用户直接加载mysite.com/picture.jpg这个文件呢?我知道用户必须知道文件和路径名称才能做到这一点,但是有没有办法也可以防止这种情况发生呢? - Michel
@michael - 你需要将媒体存储在不可通过网络访问的根目录之外。这样做意味着外部任何人都无法直接访问图像。PHP处理程序文件可以在服务器上(Web根目录之外)找到正确的文件,如果会话和安全检查得到批准,则可以将其返回。 - Shane

10

X-Sendfile

有一个Apache模块(以及其他HTTP服务器),可以让你在php代码中的标头中指定要提供的文件给HTTP服务器: 因此,您的php脚本应如下所示:

// 1) Check access rights code
// 2) If OK, tell Apache to serve the file
header("X-Sendfile: $filename");

可能会出现以下两种问题:

  1. 您需要访问重写规则(启用了.htaccess或直接访问配置文件)
  2. 您需要将mod_xsendfile模块添加到已安装的Apache中

这里是另一个帖子中的好答案:https://dev59.com/5HA65IYBdhLWcg3wqQc8#3731639


2
这绝对是解决这个问题的最佳方案。 - mathieu

5

我一直在思考同样的问题。对于每个小资源的PHP引擎运行,我同样不满意。几个月前,我曾提出了一个类似的问题(这里),尽管重点不同。

但我突然有了一个非常有趣的想法,可能会起作用。

  • 在您的Web服务器上维护一个名为/sessions的目录。

  • 每当用户登录时,在/sessions中创建一个带有会话ID的空文本文件。例如123456

  • 在您的PHP应用程序中,像这样提供图像:/sessions/123456/images/test.jpg

  • 在htaccess文件中,有两个重定向命令。

  • 一个将/sessions/123456/images/test.jpg转换为/sessions/123456?filename=images/test.jpg

  • 第二个捕捉到任何对//sessions/(.*)的调用,并使用-f标志检查指定的文件是否存在。如果不存在/sessions/123456,则表示用户已注销或其会话已过期。在这种情况下,Apache发送403或重定向到错误页面-资源不再可用。

这样,我们在mod_rewrite中拥有准会话身份验证,只需进行一次“文件存在”检查!

我没有足够的例程来动态构建mod_rewrite语句,但编写它们应该很容易。(我满怀希望地看着你,@Gumbo :)

注意和警告:

  • 必须使用cron作业快速删除过期的会话文件,除非可以在.htaccess中检查文件的修改时间(这很可能是可能的)。

  • 只要会话存在,图像/资源就对任何客户端都可用,因此没有100%的保护。您可能可以通过将客户端IP添加到方程式中(=您创建的文件名)并为%{REMOTE_ADDR}进行附加检查来解决此问题。这是高级的.htaccess技巧,但我相当确定它是可行的。

  • 资源URL不是静态的,并且必须每次在登录时检索,因此无法缓存。

我对此非常感兴趣,希望能获得反馈,以及可能忽略的任何缺陷或不可能性,以及任何成功的实现(我现在没有时间进行测试)。


4
创建一个重写映射,用于验证用户的凭据,并将其重定向到相应的资源或“访问被拒绝”页面。请参考mod_rewrite模块文档

是的!在最初撰写这个问题后,我也尝试了一下这个想法。使用RewriteMap,如果已经认证,则重写到最初请求的URL,否则重定向到拒绝页面。但是我无论如何都无法让它正常工作。当我在PRG中指定一个PHP脚本时,甚至无法启动Apache。您有使用此类技术的示例或提示吗?我的设置是:Apache 2.2 / PHP 5.2 / Windows Server 2008。 - Bart
脚本需要可执行,这意味着在Windows上必须将.php与PHP CLI可执行文件关联起来。此外,脚本在输出时应调用 flush(),并应该循环回等待进一步的输入;脚本在httpd启动时启动,在httpd关闭时被杀死。 - Ignacio Vazquez-Abrams

2
维护htaccess文件的内容似乎很困难。此外,您的目标是防止未经身份验证的用户而不是未经身份验证的客户端IP地址访问此内容 - 因此,这种方法不适用于此目的:
多个用户可能看起来来自同一IP地址。
单个用户的会话可能看起来来自多个地址。
引用:
我担心随着用户数量和他们访问的文件数量增加,我的第一个解决方案可能会在服务器上变得非常昂贵。
如果您想防止内容泄漏并且不想使用HTTP身份验证,则在所有文件访问中添加额外的逻辑层是唯一明智的选择。此外,您不知道使用PHP是否存在问题-您测试过了吗?我认为您会惊讶于它可以提供多少吞吐量,特别是如果您使用一个opcode缓存。
我猜您对包装器的“简化”处理了mime类型和缓存等问题。
C.

PHP对于访问频繁的高负载网站绝对是一个问题。这不仅仅是数据传输,还需要为每个小资源执行整个用户身份验证过程。这通常会消耗几MB的内存,并且每次请求资源时都会发生。 - Pekka
定义“重型任务”?我曾经在一台1GHz赛扬龙芯处理器,512MB内存的计算机上(实际上是4台服务器),运行更为复杂的脚本,每天处理50万次点击,并从数据库中提取数据,仍然能够保持平均响应时间为250毫秒。 - symcbean
不,我还没有测试过。但是不管细节如何,一个仅提供文件的Web服务器肯定比运行PHP执行一些文件系统功能(如“is_file”和“stat”),最后通过“readfile”提供文件要便宜得多。 - Bart
我认为,如果保护任何一个小资源,那么授权脚本请求的数量可能是真实请求的十倍或百倍。但是,你真的想要保护在设计不良的网站中充当间隔符的白色6*78像素吗?如果只保护内容,则用户想要加载的图像的身份验证将仅使服务器负担加倍。 - Andras Balázs Lajtha

0

我可能有一个建议,基于iframeHTTP_REFERER,但它并不是百分百可靠的,而且这将取决于你想用这个访问来保护什么。

但是,如果要防止未经身份验证的完整静态页面显示,可以按照以下步骤进行:

1- 使用PHP页面对用户进行身份验证

2- 重定向到另一个包含URL中键和链接到静态内容的iframe的PHP页面主体中:

<iframe src="static/content.html" />

3 - 然后在您的htaccess中,您可以像这样检查HTTP_REFERER中的密钥:

RewriteEngine On
RewriteCond %{HTTP_REFERER} !AUTH_KEY
RewriteCond %{REQUEST_URI} ^/path/to/protected/page$
RewriteRule . - [F]

4 - 最后,如果您想使它更具动态性,而不是每次使用相同的KEY,您可以使用rewrite map,如Ignacio Vazquez-Abrams的答案所建议的,或者创建一个以用户IP为文件名的文件,并使用REMOTE_ADDR检查文件是否存在,然后在一段时间后删除该文件。

但请记住,iframe + HTTP_REFERER的行为可能会因浏览器会话而异,以及REMOTE_ADDR也是如此,因此它是有限制的...


0
我编写了一个动态Web应用程序,并将其部署在Webshere应用服务器上,以下是我保护静态文件的方法:
首先,我添加了
<login-config id="LoginConfig_1">
  <auth-method>FORM</auth-method>
    <realm-name>Form-Based Authentication</realm-name>
      <form-login-config>
        <form-login-page>/login.html</form-login-page>
        <form-error-page>/login_error.html</form-error-page>
       </form-login-config>
</login-config>

在web.xml中,可以告诉您的Web服务器使用基于表单的身份验证(下面给出了登录代码)。

登录页面的代码:

<form id="form1" name="form1" method="post" action="j_security_check" style="padding: 0px 0px 0px 12px;">
        Username: 
          <label>
          <input name="j_username" type="text" class="font2" />
        </label>

        <br />
        <br />
        Password:
        <span class="font2" >
        <label>
      <input name="j_password" type="password" class="font2" />
      </label>
      </span> 
        <br />
        <br />
            <label>
        <input type="submit"  class="isc-login-button" name="Login" value="Login" />
        </label>
    </form></td>

为了实现基于表单的登录,您需要配置您的web服务器以使用特定的用户注册表,可以是LDAP或数据库。
您可以声明您的安全资源,每当用户尝试访问这些资源时,容器会自动检查用户是否经过身份验证。您甚至可以将角色附加到安全资源上。为此,我在我的web.xml文件中添加了以下代码。
<security-constraint>
        <display-name>Authenticated</display-name>
        <web-resource-collection>
            <web-resource-name>/*</web-resource-name>
            <url-pattern>/*</url-pattern>
            <http-method>GET</http-method>
            <http-method>PUT</http-method>
            <http-method>HEAD</http-method>
            <http-method>TRACE</http-method>
            <http-method>POST</http-method>
            <http-method>DELETE</http-method>
            <http-method>OPTIONS</http-method>
        </web-resource-collection>
        <auth-constraint>
            <description>Auth Roles</description>
            <role-name>role1</role-name>            
            <role-name>role2</role-name>            
        </auth-constraint>
    </security-constraint>

    <security-role>
        <role-name>role1</role-name>
    </security-role>
    <security-role>
        <role-name>role2</role-name>
    </security-role>

因此,此代码将不允许用户查看任何静态文件(因为/*),除非他在角色role1和role2下登录。通过这种方式,您可以保护您的资源。


0

这并没有完全回答问题,因为原帖作者需要那些文件能够以某种方式被授权用户访问。 - Tchoupi

0
假设您想保护所有静态文件并且必须从Web根目录内部提供它们,您可以保护除HEAD之外的所有HTTP方法。 如果您被授权,则可以使头请求通过标头并将文件内容作为正文发送。 当然,这很昂贵,但您得到了保护,并且具有相同的行为。

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